loop-memory 0.4.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.
- loop_memory/__init__.py +62 -0
- loop_memory/backends/__init__.py +13 -0
- loop_memory/backends/embedding.py +82 -0
- loop_memory/backends/sentence_embedder.py +30 -0
- loop_memory/backends/vector_store.py +139 -0
- loop_memory/cli/__init__.py +0 -0
- loop_memory/cli/_common.py +68 -0
- loop_memory/cli/commands/__init__.py +13 -0
- loop_memory/cli/commands/cognitive.py +205 -0
- loop_memory/cli/commands/diag.py +346 -0
- loop_memory/cli/commands/graph.py +21 -0
- loop_memory/cli/commands/hooks.py +212 -0
- loop_memory/cli/commands/read.py +362 -0
- loop_memory/cli/commands/serve.py +147 -0
- loop_memory/cli/commands/write.py +138 -0
- loop_memory/cli/main.py +115 -0
- loop_memory/engine/__init__.py +0 -0
- loop_memory/engine/loop.py +247 -0
- loop_memory/engine/reflect.py +89 -0
- loop_memory/examples/__init__.py +0 -0
- loop_memory/examples/demo.py +39 -0
- loop_memory/export/__init__.py +39 -0
- loop_memory/export/memory_md.py +629 -0
- loop_memory/graph/__init__.py +0 -0
- loop_memory/graph/build.py +259 -0
- loop_memory/graph/extract.py +197 -0
- loop_memory/ingest/__init__.py +0 -0
- loop_memory/ingest/loader.py +782 -0
- loop_memory/ingest/pipeline.py +458 -0
- loop_memory/jobs/__init__.py +0 -0
- loop_memory/jobs/cognitive.py +353 -0
- loop_memory/jobs/compact.py +371 -0
- loop_memory/jobs/consolidate.py +95 -0
- loop_memory/jobs/contradiction.py +281 -0
- loop_memory/jobs/evolution.py +2021 -0
- loop_memory/jobs/graph.py +395 -0
- loop_memory/jobs/llm_compact_pass.py +24 -0
- loop_memory/jobs/llm_consolidate.py +980 -0
- loop_memory/jobs/scheduler.py +495 -0
- loop_memory/llm/__init__.py +0 -0
- loop_memory/llm/base.py +80 -0
- loop_memory/llm/openai_adapter.py +31 -0
- loop_memory/llm/providers.py +517 -0
- loop_memory/mcp/__init__.py +804 -0
- loop_memory/memory/__init__.py +0 -0
- loop_memory/memory/types.py +199 -0
- loop_memory/privacy/__init__.py +22 -0
- loop_memory/privacy/private.py +46 -0
- loop_memory/privacy/redact.py +188 -0
- loop_memory/py.typed +0 -0
- loop_memory/sdk.py +875 -0
- loop_memory/sdk_extensions.py +384 -0
- loop_memory/security/__init__.py +20 -0
- loop_memory/security/secrets.py +464 -0
- loop_memory/serve/__init__.py +0 -0
- loop_memory/serve/app.py +506 -0
- loop_memory/serve/handlers.py +316 -0
- loop_memory/serve/routes/_shared.py +59 -0
- loop_memory/serve/routes/admin.py +970 -0
- loop_memory/serve/routes/cognitive.py +64 -0
- loop_memory/serve/routes/export.py +65 -0
- loop_memory/serve/routes/graph.py +101 -0
- loop_memory/serve/routes/insights.py +702 -0
- loop_memory/serve/routes/memories.py +435 -0
- loop_memory/serve/routes/sessions.py +75 -0
- loop_memory/serve/routes/system.py +493 -0
- loop_memory/serve/routes/wiki.py +812 -0
- loop_memory/serve/static/__init__.py +0 -0
- loop_memory/serve/static/index.html +15 -0
- loop_memory/serve/watcher.py +451 -0
- loop_memory/storage/__init__.py +5 -0
- loop_memory/storage/retrieval.py +365 -0
- loop_memory/storage/sqlite_store.py +3627 -0
- loop_memory/wiki/__init__.py +41 -0
- loop_memory/wiki/backfill.py +143 -0
- loop_memory/wiki/classifier.py +238 -0
- loop_memory/wiki/prompts.py +295 -0
- loop_memory/wiki/scope.py +227 -0
- loop_memory-0.4.0.dist-info/METADATA +627 -0
- loop_memory-0.4.0.dist-info/RECORD +84 -0
- loop_memory-0.4.0.dist-info/WHEEL +5 -0
- loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
- loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
- loop_memory-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Build the knowledge graph from the memories table.
|
|
2
|
+
|
|
3
|
+
``KnowledgeGraph.rebuild()`` walks every memory row, runs the
|
|
4
|
+
zero-dep entity extractor on its text, and:
|
|
5
|
+
|
|
6
|
+
1. Upserts each entity row in ``entities`` (name, kind, weight, count).
|
|
7
|
+
2. Computes pairwise co-occurrence within a sliding window per memory
|
|
8
|
+
and stores them as ``co_occurs_with`` relations in ``relations``.
|
|
9
|
+
3. Records which memory id each relation is evidenced by so the UI
|
|
10
|
+
can highlight the underlying chunks.
|
|
11
|
+
|
|
12
|
+
Plug in an LLM extractor for higher quality by replacing the ``extract``
|
|
13
|
+
function via the ``KnowledgeGraph(extractor=...)`` constructor — the
|
|
14
|
+
function signature is ``(text: str) -> list[tuple[name, kind]]``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from ..storage.sqlite_store import MemoryStore
|
|
25
|
+
from .extract import extract_entities
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
ExtractorFn = Callable[[str], list[tuple[str, str]]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class BuildReport:
|
|
35
|
+
entities: int = 0
|
|
36
|
+
relations: int = 0
|
|
37
|
+
memories_scanned: int = 0
|
|
38
|
+
elapsed_ms: float = 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class KnowledgeGraph:
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
store: MemoryStore,
|
|
45
|
+
extractor: ExtractorFn | None = None,
|
|
46
|
+
window: int = 6,
|
|
47
|
+
min_mention: int = 1,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.store = store
|
|
50
|
+
self.extractor = extractor or extract_entities
|
|
51
|
+
self.window = window
|
|
52
|
+
self.min_mention = min_mention
|
|
53
|
+
|
|
54
|
+
def rebuild(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
clear: bool = False,
|
|
58
|
+
limit: int | None = None,
|
|
59
|
+
) -> BuildReport:
|
|
60
|
+
t0 = time.time()
|
|
61
|
+
report = BuildReport()
|
|
62
|
+
if clear:
|
|
63
|
+
removed = self.store.delete_graph()
|
|
64
|
+
log.info("cleared graph: removed %s entities", removed)
|
|
65
|
+
|
|
66
|
+
rows = self.store.list_memories(limit=limit or 100_000)
|
|
67
|
+
report.memories_scanned = len(rows)
|
|
68
|
+
for row in rows:
|
|
69
|
+
ents = self.extractor(row.text or "")
|
|
70
|
+
ents = [(n, k) for (n, k) in ents if len(n) <= 32]
|
|
71
|
+
for name, kind in ents:
|
|
72
|
+
self.store.upsert_entity(name, kind, bump_weight=0.02)
|
|
73
|
+
# co-occurrence per memory text
|
|
74
|
+
if ents:
|
|
75
|
+
names = [n for (n, _) in ents]
|
|
76
|
+
pairs = self._pairs_in_window(names, window=self.window)
|
|
77
|
+
for a, b in pairs:
|
|
78
|
+
self.store.upsert_relation(
|
|
79
|
+
a, b, kind="co_occurs_with",
|
|
80
|
+
weight=0.5, evidence_id=row.id,
|
|
81
|
+
)
|
|
82
|
+
report.relations += 1
|
|
83
|
+
stats = self.store.graph_stats()
|
|
84
|
+
report.entities = stats["entities"]
|
|
85
|
+
report.relations = stats["relations"]
|
|
86
|
+
report.elapsed_ms = (time.time() - t0) * 1000
|
|
87
|
+
return report
|
|
88
|
+
|
|
89
|
+
def _pairs_in_window(self, names, *, window: int) -> list[tuple[str, str]]:
|
|
90
|
+
# Local co-occurrence within the same memory text.
|
|
91
|
+
seen = set()
|
|
92
|
+
out: list[tuple[str, str]] = []
|
|
93
|
+
for i, a in enumerate(names):
|
|
94
|
+
for b in names[i + 1 : i + window]:
|
|
95
|
+
if a == b:
|
|
96
|
+
continue
|
|
97
|
+
key = tuple(sorted([a, b]))
|
|
98
|
+
if key in seen:
|
|
99
|
+
continue
|
|
100
|
+
seen.add(key)
|
|
101
|
+
out.append((a, b))
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Wiki-based graph: build the knowledge graph from the *distilled*
|
|
106
|
+
# wiki pages rather than the raw memories. The result is denser,
|
|
107
|
+
# cleaner, and easier to navigate because every node represents a
|
|
108
|
+
# topic the user has already validated through consolidation.
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def rebuild_from_wiki(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
clear: bool = False,
|
|
115
|
+
limit: int | None = None,
|
|
116
|
+
) -> BuildReport:
|
|
117
|
+
"""Build the graph from ``wiki_pages`` instead of raw memories.
|
|
118
|
+
|
|
119
|
+
Nodes created (with ``kind`` so the UI can color them):
|
|
120
|
+
|
|
121
|
+
* ``wiki:<slug>`` — one node per wiki page
|
|
122
|
+
* ``tag:<name>`` — one node per tag used across pages
|
|
123
|
+
* ``concept:<name>`` — entities extracted from the page body
|
|
124
|
+
|
|
125
|
+
Edges created:
|
|
126
|
+
|
|
127
|
+
* ``wiki --tagged_with--> tag`` (per tag in page)
|
|
128
|
+
* ``wiki --mentions--> concept`` (entities in body)
|
|
129
|
+
* ``wiki --related_to--> wiki`` (pages sharing a tag or concept)
|
|
130
|
+
|
|
131
|
+
Each relation carries the page id (and any source-memory ids)
|
|
132
|
+
as evidence so the UI can drill back to the original chunks.
|
|
133
|
+
"""
|
|
134
|
+
t0 = time.time()
|
|
135
|
+
report = BuildReport()
|
|
136
|
+
if clear:
|
|
137
|
+
removed = self.store.delete_graph()
|
|
138
|
+
log.info("cleared graph: removed %s entities", removed)
|
|
139
|
+
|
|
140
|
+
pages = self.store.list_wiki_pages(limit=limit or 1000)
|
|
141
|
+
report.memories_scanned = len(pages)
|
|
142
|
+
if not pages:
|
|
143
|
+
log.info("rebuild_from_wiki: no wiki pages; skipping")
|
|
144
|
+
report.elapsed_ms = (time.time() - t0) * 1000
|
|
145
|
+
return report
|
|
146
|
+
|
|
147
|
+
# 1) Page nodes
|
|
148
|
+
page_node_ids: Dict[str, str] = {}
|
|
149
|
+
for page in pages:
|
|
150
|
+
slug = (page.get("slug") or "").strip()
|
|
151
|
+
if not slug:
|
|
152
|
+
continue
|
|
153
|
+
importance = float(page.get("importance") or 0.5)
|
|
154
|
+
# Wiki nodes start at importance and accumulate tiny weight
|
|
155
|
+
# from shared tags/concepts — they should dominate visually.
|
|
156
|
+
ent = self.store.upsert_entity(
|
|
157
|
+
f"wiki:{slug}", kind="wiki_page",
|
|
158
|
+
bump_weight=max(0.0, importance - 0.5),
|
|
159
|
+
)
|
|
160
|
+
page_node_ids[slug] = ent.name
|
|
161
|
+
# Bump by mention_count too so a page with N tags/concepts
|
|
162
|
+
# is more prominent than a lonely one.
|
|
163
|
+
for _ in range(min(8, len(page.get("tags") or []) + 1)):
|
|
164
|
+
self.store.upsert_entity(
|
|
165
|
+
f"wiki:{slug}", kind="wiki_page", bump_weight=0.01,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 2) Tag nodes + tag edges
|
|
169
|
+
tag_to_pages: Dict[str, list[str]] = {}
|
|
170
|
+
for page in pages:
|
|
171
|
+
tags = page.get("tags") or []
|
|
172
|
+
slug = page.get("slug") or ""
|
|
173
|
+
if not slug:
|
|
174
|
+
continue
|
|
175
|
+
for raw in tags:
|
|
176
|
+
tag = (raw or "").strip().lower()
|
|
177
|
+
if not tag:
|
|
178
|
+
continue
|
|
179
|
+
self.store.upsert_entity(
|
|
180
|
+
f"tag:{tag}", kind="tag", bump_weight=0.05,
|
|
181
|
+
)
|
|
182
|
+
self.store.upsert_relation(
|
|
183
|
+
f"wiki:{slug}", f"tag:{tag}",
|
|
184
|
+
kind="tagged_with",
|
|
185
|
+
weight=0.7,
|
|
186
|
+
evidence_id=page.get("id"),
|
|
187
|
+
)
|
|
188
|
+
report.relations += 1
|
|
189
|
+
tag_to_pages.setdefault(tag, []).append(slug)
|
|
190
|
+
|
|
191
|
+
# 3) Concept entities from page body + title + summary
|
|
192
|
+
concept_to_pages: Dict[str, list[str]] = {}
|
|
193
|
+
for page in pages:
|
|
194
|
+
slug = page.get("slug") or ""
|
|
195
|
+
if not slug:
|
|
196
|
+
continue
|
|
197
|
+
text_chunks = [
|
|
198
|
+
page.get("title") or "",
|
|
199
|
+
page.get("summary") or "",
|
|
200
|
+
page.get("body") or "",
|
|
201
|
+
]
|
|
202
|
+
text = chr(10).join(t for t in text_chunks if t)
|
|
203
|
+
ents = self.extractor(text)
|
|
204
|
+
seen_here: set = set()
|
|
205
|
+
for name, _kind in ents:
|
|
206
|
+
name = (name or "").strip()
|
|
207
|
+
if not name or len(name) > 32:
|
|
208
|
+
continue
|
|
209
|
+
if name in seen_here:
|
|
210
|
+
continue
|
|
211
|
+
seen_here.add(name)
|
|
212
|
+
self.store.upsert_entity(
|
|
213
|
+
f"concept:{name}", kind="concept", bump_weight=0.02,
|
|
214
|
+
)
|
|
215
|
+
self.store.upsert_relation(
|
|
216
|
+
f"wiki:{slug}", f"concept:{name}",
|
|
217
|
+
kind="mentions",
|
|
218
|
+
weight=0.4,
|
|
219
|
+
evidence_id=page.get("id"),
|
|
220
|
+
)
|
|
221
|
+
report.relations += 1
|
|
222
|
+
concept_to_pages.setdefault(name, []).append(slug)
|
|
223
|
+
|
|
224
|
+
# 4) Wiki --related_to--> wiki when pages share a tag or concept
|
|
225
|
+
def _relate(a: str, b: str, evidence_id: str | None) -> None:
|
|
226
|
+
if a == b:
|
|
227
|
+
return
|
|
228
|
+
# Insert in both directions so the UI can highlight a↔b
|
|
229
|
+
# without having to do its own lookup.
|
|
230
|
+
for x, y in ((a, b), (b, a)):
|
|
231
|
+
self.store.upsert_relation(
|
|
232
|
+
f"wiki:{x}", f"wiki:{y}",
|
|
233
|
+
kind="related_to",
|
|
234
|
+
weight=0.3,
|
|
235
|
+
evidence_id=evidence_id,
|
|
236
|
+
)
|
|
237
|
+
report.relations += 1
|
|
238
|
+
|
|
239
|
+
for tag, slugs in tag_to_pages.items():
|
|
240
|
+
slugs = list(dict.fromkeys(slugs))
|
|
241
|
+
for i, a in enumerate(slugs):
|
|
242
|
+
for b in slugs[i + 1:]:
|
|
243
|
+
_relate(a, b, evidence_id=None)
|
|
244
|
+
for _concept, slugs in concept_to_pages.items():
|
|
245
|
+
slugs = list(dict.fromkeys(slugs))
|
|
246
|
+
if len(slugs) <= 8:
|
|
247
|
+
for i, a in enumerate(slugs):
|
|
248
|
+
for b in slugs[i + 1:]:
|
|
249
|
+
_relate(a, b, evidence_id=None)
|
|
250
|
+
|
|
251
|
+
stats = self.store.graph_stats()
|
|
252
|
+
report.entities = stats["entities"]
|
|
253
|
+
report.relations = stats["relations"]
|
|
254
|
+
report.elapsed_ms = (time.time() - t0) * 1000
|
|
255
|
+
log.info(
|
|
256
|
+
"rebuild_from_wiki: %d pages -> %d entities, %d relations in %.1fms",
|
|
257
|
+
len(pages), report.entities, report.relations, report.elapsed_ms,
|
|
258
|
+
)
|
|
259
|
+
return report
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Lightweight entity + co-occurrence extractor (zero-dep, heuristic).
|
|
2
|
+
|
|
3
|
+
We don't pull in spaCy here. This module produces two things:
|
|
4
|
+
|
|
5
|
+
* ``extract_entities(text)`` → list of (name, kind) candidates, deduped
|
|
6
|
+
and filtered by stopwords. Recognised entities include Capitalised
|
|
7
|
+
Latin tokens, ``#hashtag`` style tokens, file paths / URLs, CamelCase
|
|
8
|
+
product names, and short CJK noun-like runs (after stopword filter).
|
|
9
|
+
|
|
10
|
+
* ``pair_cooccurrence(texts, window=...)`` → list of (a, b, count)
|
|
11
|
+
relations where ``a`` and ``b`` appear within ``window`` tokens of
|
|
12
|
+
each other in any of the input texts.
|
|
13
|
+
|
|
14
|
+
This is intentionally tunable — for higher quality call out to an
|
|
15
|
+
LLM-backed extractor and feed the output through the same API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from collections import Counter
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
|
|
24
|
+
# --- tokenisation -----------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_TOKEN_RE = re.compile(
|
|
27
|
+
r"[A-Za-z][A-Za-z0-9_]+" # Latin word
|
|
28
|
+
r"|\#[\w\u4e00-\u9fff]+" # #hashtag (English / CJK mix)
|
|
29
|
+
r"|https?://[^\s]+" # URL
|
|
30
|
+
r"|[A-Za-z0-9_./-]+\.[A-Za-z0-9]{2,}" # file.ext or domain.tld
|
|
31
|
+
r"|[\u4e00-\u9fff]{2,8}" # CJK noun runs (2–8 chars)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tokenize(text: str) -> list[str]:
|
|
36
|
+
return _TOKEN_RE.findall(text or "")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- stopword lists ---------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
_EN_STOPWORDS = {
|
|
42
|
+
"the", "and", "for", "with", "from", "into", "this", "that", "these",
|
|
43
|
+
"those", "you", "your", "are", "was", "were", "have", "has", "had",
|
|
44
|
+
"but", "not", "any", "all", "can", "could", "would", "should", "will",
|
|
45
|
+
"shall", "may", "might", "must", "what", "why", "how", "who", "where",
|
|
46
|
+
"when", "then", "now", "also", "still", "just", "very", "more",
|
|
47
|
+
"less", "most", "least", "out", "off", "per", "via", "i", "me",
|
|
48
|
+
"my", "we", "us", "our", "they", "their", "them", "he", "she", "his",
|
|
49
|
+
"her", "is", "am", "be", "been", "being", "do", "does", "did", "done",
|
|
50
|
+
"doing", "of", "in", "on", "at", "to", "by", "as", "an", "or", "if",
|
|
51
|
+
"no", "yes", "so", "it", "its", "about", "than", "there", "here",
|
|
52
|
+
"above", "below", "under", "over", "again", "once", "each",
|
|
53
|
+
"something", "anything", "everything", "nothing", "some",
|
|
54
|
+
"which", "such", "after", "before",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Common short CJK fragments that aren't real concepts.
|
|
58
|
+
_CJK_STOPWORDS = {
|
|
59
|
+
"的", "了", "和", "是", "在", "我", "你", "他", "她", "它",
|
|
60
|
+
"我们", "你们", "他们", "这个", "那个", "什么", "怎么", "为什么",
|
|
61
|
+
"可以", "可能", "也许", "应该", "已经", "现在", "之前", "之后",
|
|
62
|
+
"因为", "所以", "如果", "但是", "不过", "然后", "可是", "而且",
|
|
63
|
+
"或者", "还有", "也", "都", "就", "才", "只", "再", "又", "很",
|
|
64
|
+
"非常", "比较", "一点", "一下", "一直", "顺便", "帮我", "我用",
|
|
65
|
+
"你用", "对她", "我对", "是不是",
|
|
66
|
+
"用", "打", "做", "搞", "弄", "给", "让", "把", "被", "由",
|
|
67
|
+
"从", "到", "向", "对", "跟", "比", "如", "若", "虽", "除非",
|
|
68
|
+
"将", "会", "能", "须", "必", "得", "地", "着", "过", "如何", "怎样", "为啥", "的工具", "的项目", "的系统", "的代码", "的内容", "的功能",
|
|
69
|
+
"一个", "一些", "这些", "那些", "今天",
|
|
70
|
+
"明天", "昨天", "谢谢", "感谢", "麻烦", "请帮",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_STOPWORDS = _EN_STOPWORDS
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- detection --------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Extra stopwords: tokens that come up very frequently as filler in
|
|
82
|
+
# extracted LLM-style text — they tend to drown out useful entities.
|
|
83
|
+
_GENERIC_TERMS = {
|
|
84
|
+
"User", "Users", "Assistant", "Outcome", "Task", "Description",
|
|
85
|
+
"Issues", "Issue", "Files", "File", "Strengths", "Added",
|
|
86
|
+
"Required", "Implemented", "Complete", "Check", "Review",
|
|
87
|
+
"Code", "Title", "Body", "Read", "Note", "Source", "Target", "Message", "Context", "Result", "Output", "Input", "Step", "Steps", "List", "Section",
|
|
88
|
+
"Project", "Repository", "Repo", "Doc", "Documentation",
|
|
89
|
+
"Docs", "URL", "Path", "Line", "Lines", "Type", "Method",
|
|
90
|
+
"Function", "Class", "Module", "Package", "Import", "Export",
|
|
91
|
+
"Example", "Examples", "Sample", "Demo", "Use", "Using", "Used",
|
|
92
|
+
"Make", "Made", "Run", "Running", "Create", "Creates",
|
|
93
|
+
"Created", "Add", "Adding", "Remove", "Removed",
|
|
94
|
+
"Removing", "Update", "Updated", "Updating", "Change", "Changes",
|
|
95
|
+
"Changed", "Show", "Showing", "Found", "Founding", "Missing",
|
|
96
|
+
"First", "Second", "Third",
|
|
97
|
+
"Test", "Tests", "Tested", "Testing", "Pass", "Passes",
|
|
98
|
+
"Status", "OK", "Ok", "Notes", "Comments", "Comment",
|
|
99
|
+
"Done", "Completed", "Completion",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def _looks_like_proper_noun(tok: str) -> bool:
|
|
103
|
+
"""True for tokens that may be useful named-entity material."""
|
|
104
|
+
if len(tok) < 2:
|
|
105
|
+
return False
|
|
106
|
+
if tok.isupper() and len(tok) <= 5:
|
|
107
|
+
return tok.isalpha() # ACRONYM
|
|
108
|
+
if tok[0].isupper() and any(c.islower() for c in tok[1:]):
|
|
109
|
+
return True
|
|
110
|
+
if any(c.isupper() for c in tok[1:]) and any(c.islower() for c in tok):
|
|
111
|
+
return True # CamelCase
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _kind_for(tok: str) -> str:
|
|
116
|
+
if tok.startswith("#"):
|
|
117
|
+
return "tag"
|
|
118
|
+
if tok.startswith("http"):
|
|
119
|
+
return "url"
|
|
120
|
+
if "." in tok and "/" in tok:
|
|
121
|
+
return "path"
|
|
122
|
+
if tok.isupper():
|
|
123
|
+
return "acronym"
|
|
124
|
+
if any(ord(c) > 127 for c in tok):
|
|
125
|
+
return "cjk"
|
|
126
|
+
return "concept"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Heuristic disqualifiers for CJK tokens: any of these characters in
|
|
130
|
+
# any position is a strong signal the n-gram is not a real concept.
|
|
131
|
+
_CJK_FRAGMENT_MARKERS = set("的了着过得把被让给向将从跟比和或而但所以因为") | set("是吗呀啊嘛呢哦嗯哈呀嘛啊")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _is_useful(tok: str) -> bool:
|
|
135
|
+
if not tok:
|
|
136
|
+
return False
|
|
137
|
+
lower = tok.lower()
|
|
138
|
+
if lower in _STOPWORDS:
|
|
139
|
+
return False
|
|
140
|
+
if tok in _CJK_STOPWORDS:
|
|
141
|
+
return False
|
|
142
|
+
if tok in _GENERIC_TERMS:
|
|
143
|
+
return False
|
|
144
|
+
# Latin: only proper-nounish tokens qualify.
|
|
145
|
+
if ord(tok[0]) < 128:
|
|
146
|
+
if not _looks_like_proper_noun(tok):
|
|
147
|
+
return False
|
|
148
|
+
return True
|
|
149
|
+
# CJK token refinement
|
|
150
|
+
if any(c in _CJK_FRAGMENT_MARKERS for c in tok):
|
|
151
|
+
return False
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def extract_entities(text: str, *, min_count: int = 1) -> list[tuple[str, str]]:
|
|
156
|
+
if not text:
|
|
157
|
+
return []
|
|
158
|
+
counts: Counter = Counter()
|
|
159
|
+
for tok in tokenize(text):
|
|
160
|
+
tok = tok.strip("'\"`")
|
|
161
|
+
if _is_useful(tok):
|
|
162
|
+
counts[tok] += 1
|
|
163
|
+
return [
|
|
164
|
+
(tok, _kind_for(tok))
|
|
165
|
+
for tok, n in counts.items()
|
|
166
|
+
if n >= min_count
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def pair_cooccurrence(
|
|
171
|
+
texts: Iterable[str],
|
|
172
|
+
*,
|
|
173
|
+
window: int = 6,
|
|
174
|
+
min_count: int = 1,
|
|
175
|
+
) -> list[tuple[str, str, int]]:
|
|
176
|
+
pair_counts: Counter = Counter()
|
|
177
|
+
for text in texts:
|
|
178
|
+
toks = [tok for tok in tokenize(text) if _is_useful(tok)]
|
|
179
|
+
seen_local: set = set()
|
|
180
|
+
for i, tok in enumerate(toks):
|
|
181
|
+
for other in toks[i + 1 : i + window]:
|
|
182
|
+
if other == tok:
|
|
183
|
+
continue
|
|
184
|
+
key = tuple(sorted([tok, other]))
|
|
185
|
+
if key in seen_local:
|
|
186
|
+
continue
|
|
187
|
+
seen_local.add(key)
|
|
188
|
+
pair_counts[key] += 1
|
|
189
|
+
return [(a, b, c) for (a, b), c in pair_counts.items() if c >= min_count]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def canonical(name: str) -> str:
|
|
193
|
+
if not name:
|
|
194
|
+
return name
|
|
195
|
+
if ord(name[0]) >= 128:
|
|
196
|
+
return name
|
|
197
|
+
return name.strip()
|
|
File without changes
|