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.
- visp_memory/__init__.py +35 -0
- visp_memory/capture/__init__.py +12 -0
- visp_memory/capture/bootstrap.py +366 -0
- visp_memory/capture/conversation.py +185 -0
- visp_memory/capture/git.py +620 -0
- visp_memory/capture/instructions.py +181 -0
- visp_memory/capture/tests.py +113 -0
- visp_memory/config.py +421 -0
- visp_memory/core/__init__.py +15 -0
- visp_memory/core/anchors.py +216 -0
- visp_memory/core/arcadedb_storage.py +1355 -0
- visp_memory/core/clock.py +58 -0
- visp_memory/core/compression.py +574 -0
- visp_memory/core/context_compiler.py +156 -0
- visp_memory/core/cross_repo.py +131 -0
- visp_memory/core/embeddings.py +388 -0
- visp_memory/core/hybrid_retrieval.py +351 -0
- visp_memory/core/indexing.py +121 -0
- visp_memory/core/injection.py +649 -0
- visp_memory/core/intent_evaluator.py +187 -0
- visp_memory/core/lifecycle.py +530 -0
- visp_memory/core/llm.py +96 -0
- visp_memory/core/memory.py +1096 -0
- visp_memory/core/memory_context.py +121 -0
- visp_memory/core/memory_import_export.py +180 -0
- visp_memory/core/model_router.py +126 -0
- visp_memory/core/neo4j_storage.py +1648 -0
- visp_memory/core/ranking.py +344 -0
- visp_memory/core/reflection.py +130 -0
- visp_memory/core/remote_storage.py +559 -0
- visp_memory/core/reporting.py +428 -0
- visp_memory/core/repository.py +148 -0
- visp_memory/core/storage.py +2605 -0
- visp_memory/core/task_brief.py +542 -0
- visp_memory/core/team.py +115 -0
- visp_memory/core/tokens.py +138 -0
- visp_memory/core/trust.py +231 -0
- visp_memory/hooks/__init__.py +52 -0
- visp_memory/hooks/aider.py +112 -0
- visp_memory/hooks/base.py +172 -0
- visp_memory/hooks/claude_code.py +140 -0
- visp_memory/hooks/claude_code_auto.py +277 -0
- visp_memory/hooks/codex.py +247 -0
- visp_memory/hooks/cursor.py +113 -0
- visp_memory/hooks/generic.py +208 -0
- visp_memory/interfaces/__init__.py +5 -0
- visp_memory/interfaces/cli.py +2693 -0
- visp_memory/interfaces/mcp.py +2104 -0
- visp_memory/layers/__init__.py +13 -0
- visp_memory/layers/base.py +71 -0
- visp_memory/layers/episodic.py +272 -0
- visp_memory/layers/intent.py +302 -0
- visp_memory/layers/semantic.py +327 -0
- visp_memory/quality/__init__.py +3 -0
- visp_memory/quality/conflict.py +104 -0
- visp_memory/quality/dedup.py +341 -0
- visp_memory/quality/reconcile.py +160 -0
- visp_memory/quality/secrets.py +230 -0
- visp_memory/recall/__init__.py +13 -0
- visp_memory/recall/graph.py +623 -0
- visp_memory/recall/proactive.py +450 -0
- visp_memory/server/__init__.py +3 -0
- visp_memory/server/app.py +685 -0
- visp_memory/server/auth.py +207 -0
- visp_memory/server/auth_store.py +426 -0
- visp_memory/server/authorization.py +92 -0
- visp_memory/server/routers/__init__.py +8 -0
- visp_memory/server/routers/ai.py +205 -0
- visp_memory/server/routers/authentication.py +302 -0
- visp_memory/server/routers/context.py +66 -0
- visp_memory/server/routers/diagnostics.py +385 -0
- visp_memory/server/routers/intents.py +286 -0
- visp_memory/server/routers/memories.py +693 -0
- visp_memory/server/routers/platform.py +88 -0
- visp_memory/server/routers/quality.py +201 -0
- visp_memory/server/routers/relationships.py +120 -0
- visp_memory/server/routers/repositories.py +389 -0
- visp_memory/server/routers/sessions.py +75 -0
- visp_memory/server/routers/teams.py +191 -0
- visp_memory/server/schemas.py +539 -0
- visp_memory/server/static/404.html +1 -0
- visp_memory/server/static/__next.__PAGE__.txt +9 -0
- visp_memory/server/static/__next._full.txt +19 -0
- visp_memory/server/static/__next._head.txt +5 -0
- visp_memory/server/static/__next._index.txt +7 -0
- visp_memory/server/static/__next._tree.txt +2 -0
- visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_buildManifest.js +11 -0
- visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_clientMiddlewareManifest.js +1 -0
- visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_ssgManifest.js +1 -0
- visp_memory/server/static/_next/static/chunks/008yi10v1q4dx.js +2 -0
- visp_memory/server/static/_next/static/chunks/05-c3ty_6dwfk.js +1 -0
- visp_memory/server/static/_next/static/chunks/07-r-9f0qrcuh.js +1 -0
- visp_memory/server/static/_next/static/chunks/0_8y92xg60ach.js +31 -0
- visp_memory/server/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- visp_memory/server/static/_next/static/chunks/0ha5sx_ga-3xm.css +1 -0
- visp_memory/server/static/_next/static/chunks/0ihy_oxbd_ql7.js +1 -0
- visp_memory/server/static/_next/static/chunks/0sbgmos1me2w7.js +1 -0
- visp_memory/server/static/_next/static/chunks/10mw-ef74o_0e.js +1 -0
- visp_memory/server/static/_next/static/chunks/16wtvn_4n5ryb.js +1 -0
- visp_memory/server/static/_next/static/chunks/181fjtiwe9leb.js +1 -0
- visp_memory/server/static/_next/static/chunks/1os6n3cys6qq2.js +1 -0
- visp_memory/server/static/_next/static/chunks/229t_xxklg5dq.js +1 -0
- visp_memory/server/static/_next/static/chunks/23x4zp3dciho5.js +1 -0
- visp_memory/server/static/_next/static/chunks/25o46h8mdjlrg.js +1 -0
- visp_memory/server/static/_next/static/chunks/27jktro2p5rq9.js +4 -0
- visp_memory/server/static/_next/static/chunks/28beg3dm4qs6d.js +9 -0
- visp_memory/server/static/_next/static/chunks/2hkl13_hoi6rf.js +1 -0
- visp_memory/server/static/_next/static/chunks/2otspn0_vpeiy.js +1 -0
- visp_memory/server/static/_next/static/chunks/2xzui1nn6_dgk.js +1 -0
- visp_memory/server/static/_next/static/chunks/3924qjq6v2pkr.js +5 -0
- visp_memory/server/static/_next/static/chunks/3q-_2alq3i82j.js +1 -0
- visp_memory/server/static/_next/static/chunks/3sdblimdmjxu9.js +1 -0
- visp_memory/server/static/_next/static/chunks/3xdh3y-yb-_j8.js +1 -0
- visp_memory/server/static/_next/static/chunks/44rth4b4ij8_e.js +1 -0
- visp_memory/server/static/_next/static/chunks/turbopack-37zifqtg7l6ev.js +1 -0
- visp_memory/server/static/_not-found/__next._full.txt +17 -0
- visp_memory/server/static/_not-found/__next._head.txt +5 -0
- visp_memory/server/static/_not-found/__next._index.txt +7 -0
- visp_memory/server/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
- visp_memory/server/static/_not-found/__next._not-found.txt +5 -0
- visp_memory/server/static/_not-found/__next._tree.txt +2 -0
- visp_memory/server/static/_not-found.html +1 -0
- visp_memory/server/static/_not-found.txt +17 -0
- visp_memory/server/static/apple-icon.png +0 -0
- visp_memory/server/static/auth/__next._full.txt +21 -0
- visp_memory/server/static/auth/__next._head.txt +5 -0
- visp_memory/server/static/auth/__next._index.txt +7 -0
- visp_memory/server/static/auth/__next._tree.txt +2 -0
- visp_memory/server/static/auth/__next.auth.__PAGE__.txt +9 -0
- visp_memory/server/static/auth/__next.auth.txt +5 -0
- visp_memory/server/static/auth.html +1 -0
- visp_memory/server/static/auth.txt +21 -0
- visp_memory/server/static/brief/__next._full.txt +21 -0
- visp_memory/server/static/brief/__next._head.txt +5 -0
- visp_memory/server/static/brief/__next._index.txt +7 -0
- visp_memory/server/static/brief/__next._tree.txt +2 -0
- visp_memory/server/static/brief/__next.brief.__PAGE__.txt +9 -0
- visp_memory/server/static/brief/__next.brief.txt +5 -0
- visp_memory/server/static/brief.html +1 -0
- visp_memory/server/static/brief.txt +21 -0
- visp_memory/server/static/graph/__next._full.txt +21 -0
- visp_memory/server/static/graph/__next._head.txt +5 -0
- visp_memory/server/static/graph/__next._index.txt +7 -0
- visp_memory/server/static/graph/__next._tree.txt +2 -0
- visp_memory/server/static/graph/__next.graph.__PAGE__.txt +9 -0
- visp_memory/server/static/graph/__next.graph.txt +5 -0
- visp_memory/server/static/graph.html +1 -0
- visp_memory/server/static/graph.txt +21 -0
- visp_memory/server/static/health/__next._full.txt +21 -0
- visp_memory/server/static/health/__next._head.txt +5 -0
- visp_memory/server/static/health/__next._index.txt +7 -0
- visp_memory/server/static/health/__next._tree.txt +2 -0
- visp_memory/server/static/health/__next.health.__PAGE__.txt +9 -0
- visp_memory/server/static/health/__next.health.txt +5 -0
- visp_memory/server/static/health.html +1 -0
- visp_memory/server/static/health.txt +21 -0
- visp_memory/server/static/icon-dark-32x32.png +0 -0
- visp_memory/server/static/icon-light-32x32.png +0 -0
- visp_memory/server/static/icon.svg +26 -0
- visp_memory/server/static/index.html +1 -0
- visp_memory/server/static/index.txt +19 -0
- visp_memory/server/static/integrations/__next._full.txt +21 -0
- visp_memory/server/static/integrations/__next._head.txt +5 -0
- visp_memory/server/static/integrations/__next._index.txt +7 -0
- visp_memory/server/static/integrations/__next._tree.txt +2 -0
- visp_memory/server/static/integrations/__next.integrations.__PAGE__.txt +9 -0
- visp_memory/server/static/integrations/__next.integrations.txt +5 -0
- visp_memory/server/static/integrations.html +1 -0
- visp_memory/server/static/integrations.txt +21 -0
- visp_memory/server/static/intelligence/__next._full.txt +21 -0
- visp_memory/server/static/intelligence/__next._head.txt +5 -0
- visp_memory/server/static/intelligence/__next._index.txt +7 -0
- visp_memory/server/static/intelligence/__next._tree.txt +2 -0
- visp_memory/server/static/intelligence/__next.intelligence.__PAGE__.txt +9 -0
- visp_memory/server/static/intelligence/__next.intelligence.txt +5 -0
- visp_memory/server/static/intelligence.html +1 -0
- visp_memory/server/static/intelligence.txt +21 -0
- visp_memory/server/static/intents/__next._full.txt +21 -0
- visp_memory/server/static/intents/__next._head.txt +5 -0
- visp_memory/server/static/intents/__next._index.txt +7 -0
- visp_memory/server/static/intents/__next._tree.txt +2 -0
- visp_memory/server/static/intents/__next.intents.__PAGE__.txt +9 -0
- visp_memory/server/static/intents/__next.intents.txt +5 -0
- visp_memory/server/static/intents.html +1 -0
- visp_memory/server/static/intents.txt +21 -0
- visp_memory/server/static/memories/__next._full.txt +21 -0
- visp_memory/server/static/memories/__next._head.txt +5 -0
- visp_memory/server/static/memories/__next._index.txt +7 -0
- visp_memory/server/static/memories/__next._tree.txt +2 -0
- visp_memory/server/static/memories/__next.memories.__PAGE__.txt +9 -0
- visp_memory/server/static/memories/__next.memories.txt +5 -0
- visp_memory/server/static/memories.html +1 -0
- visp_memory/server/static/memories.txt +21 -0
- visp_memory/server/static/projects/__next._full.txt +21 -0
- visp_memory/server/static/projects/__next._head.txt +5 -0
- visp_memory/server/static/projects/__next._index.txt +7 -0
- visp_memory/server/static/projects/__next._tree.txt +2 -0
- visp_memory/server/static/projects/__next.projects.__PAGE__.txt +9 -0
- visp_memory/server/static/projects/__next.projects.txt +5 -0
- visp_memory/server/static/projects.html +1 -0
- visp_memory/server/static/projects.txt +21 -0
- visp_memory/server/static/recall/__next._full.txt +22 -0
- visp_memory/server/static/recall/__next._head.txt +5 -0
- visp_memory/server/static/recall/__next._index.txt +7 -0
- visp_memory/server/static/recall/__next._tree.txt +2 -0
- visp_memory/server/static/recall/__next.recall.__PAGE__.txt +9 -0
- visp_memory/server/static/recall/__next.recall.txt +6 -0
- visp_memory/server/static/recall.html +1 -0
- visp_memory/server/static/recall.txt +22 -0
- visp_memory/server/static/settings/__next._full.txt +21 -0
- visp_memory/server/static/settings/__next._head.txt +5 -0
- visp_memory/server/static/settings/__next._index.txt +7 -0
- visp_memory/server/static/settings/__next._tree.txt +2 -0
- visp_memory/server/static/settings/__next.settings.__PAGE__.txt +9 -0
- visp_memory/server/static/settings/__next.settings.txt +5 -0
- visp_memory/server/static/settings.html +1 -0
- visp_memory/server/static/settings.txt +21 -0
- visp_memory/server/static/users/__next._full.txt +21 -0
- visp_memory/server/static/users/__next._head.txt +5 -0
- visp_memory/server/static/users/__next._index.txt +7 -0
- visp_memory/server/static/users/__next._tree.txt +2 -0
- visp_memory/server/static/users/__next.users.__PAGE__.txt +9 -0
- visp_memory/server/static/users/__next.users.txt +5 -0
- visp_memory/server/static/users.html +1 -0
- visp_memory/server/static/users.txt +21 -0
- visp_memory-0.3.0.dist-info/METADATA +818 -0
- visp_memory-0.3.0.dist-info/RECORD +231 -0
- visp_memory-0.3.0.dist-info/WHEEL +4 -0
- visp_memory-0.3.0.dist-info/entry_points.txt +3 -0
- visp_memory-0.3.0.dist-info/licenses/LICENSE +202 -0
- visp_memory-0.3.0.dist-info/licenses/NOTICE +15 -0
visp_memory/__init__.py
ADDED
|
@@ -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
|