nexus-cli 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.
- nexus/__init__.py +8 -0
- nexus/cli.py +1914 -0
- nexus/integrations/__init__.py +0 -0
- nexus/knowledge/__init__.py +13 -0
- nexus/knowledge/search.py +233 -0
- nexus/knowledge/vault.py +662 -0
- nexus/research/__init__.py +12 -0
- nexus/research/pdf.py +497 -0
- nexus/research/zotero.py +521 -0
- nexus/teaching/__init__.py +14 -0
- nexus/teaching/courses.py +388 -0
- nexus/teaching/quarto.py +385 -0
- nexus/utils/__init__.py +0 -0
- nexus/utils/config.py +157 -0
- nexus/writing/__init__.py +12 -0
- nexus/writing/bibliography.py +339 -0
- nexus/writing/manuscript.py +397 -0
- nexus_cli-0.3.0.dist-info/METADATA +369 -0
- nexus_cli-0.3.0.dist-info/RECORD +21 -0
- nexus_cli-0.3.0.dist-info/WHEEL +4 -0
- nexus_cli-0.3.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Knowledge domain - Vault, search, and connections."""
|
|
2
|
+
|
|
3
|
+
from nexus.knowledge.search import SearchSource, UnifiedSearch, UnifiedSearchResult
|
|
4
|
+
from nexus.knowledge.vault import Note, SearchResult, VaultManager
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"VaultManager",
|
|
8
|
+
"Note",
|
|
9
|
+
"SearchResult",
|
|
10
|
+
"UnifiedSearch",
|
|
11
|
+
"UnifiedSearchResult",
|
|
12
|
+
"SearchSource",
|
|
13
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Unified search across all knowledge sources."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from nexus.knowledge.vault import VaultManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchSource(str, Enum):
|
|
11
|
+
"""Available search sources."""
|
|
12
|
+
|
|
13
|
+
VAULT = "vault"
|
|
14
|
+
ZOTERO = "zotero"
|
|
15
|
+
PDF = "pdf"
|
|
16
|
+
ALL = "all"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class UnifiedSearchResult:
|
|
21
|
+
"""A unified search result from any source."""
|
|
22
|
+
|
|
23
|
+
source: str
|
|
24
|
+
path: str
|
|
25
|
+
title: str
|
|
26
|
+
snippet: str
|
|
27
|
+
score: float = 0.0
|
|
28
|
+
metadata: dict = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
"""Convert to dictionary."""
|
|
32
|
+
return {
|
|
33
|
+
"source": self.source,
|
|
34
|
+
"path": self.path,
|
|
35
|
+
"title": self.title,
|
|
36
|
+
"snippet": self.snippet,
|
|
37
|
+
"score": self.score,
|
|
38
|
+
"metadata": self.metadata,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnifiedSearch:
|
|
43
|
+
"""Unified search across all knowledge sources."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
vault_path: Path | None = None,
|
|
48
|
+
zotero_db: Path | None = None,
|
|
49
|
+
pdf_dirs: list[Path] | None = None,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize search with available sources.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
vault_path: Path to Obsidian vault
|
|
55
|
+
zotero_db: Path to Zotero SQLite database
|
|
56
|
+
pdf_dirs: List of directories containing PDFs
|
|
57
|
+
"""
|
|
58
|
+
self.vault_path = Path(vault_path).expanduser() if vault_path else None
|
|
59
|
+
self.zotero_db = Path(zotero_db).expanduser() if zotero_db else None
|
|
60
|
+
self.pdf_dirs = [Path(d).expanduser() for d in pdf_dirs] if pdf_dirs else []
|
|
61
|
+
|
|
62
|
+
# Initialize managers
|
|
63
|
+
self._vault_manager: VaultManager | None = None
|
|
64
|
+
if self.vault_path and self.vault_path.exists():
|
|
65
|
+
self._vault_manager = VaultManager(self.vault_path)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def vault(self) -> VaultManager | None:
|
|
69
|
+
"""Get vault manager."""
|
|
70
|
+
return self._vault_manager
|
|
71
|
+
|
|
72
|
+
def available_sources(self) -> list[str]:
|
|
73
|
+
"""Get list of available search sources."""
|
|
74
|
+
sources = []
|
|
75
|
+
|
|
76
|
+
if self._vault_manager and self._vault_manager.exists():
|
|
77
|
+
sources.append("vault")
|
|
78
|
+
|
|
79
|
+
if self.zotero_db and self.zotero_db.exists():
|
|
80
|
+
sources.append("zotero")
|
|
81
|
+
|
|
82
|
+
for pdf_dir in self.pdf_dirs:
|
|
83
|
+
if pdf_dir.exists():
|
|
84
|
+
sources.append("pdf")
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
return sources
|
|
88
|
+
|
|
89
|
+
def search(
|
|
90
|
+
self,
|
|
91
|
+
query: str,
|
|
92
|
+
sources: list[str] | None = None,
|
|
93
|
+
limit: int = 20,
|
|
94
|
+
) -> list[UnifiedSearchResult]:
|
|
95
|
+
"""Search across specified sources.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
query: Search query
|
|
99
|
+
sources: List of sources to search (default: all available)
|
|
100
|
+
limit: Maximum results per source
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of unified search results
|
|
104
|
+
"""
|
|
105
|
+
if sources is None:
|
|
106
|
+
sources = self.available_sources()
|
|
107
|
+
|
|
108
|
+
results: list[UnifiedSearchResult] = []
|
|
109
|
+
|
|
110
|
+
# Search each source
|
|
111
|
+
for source in sources:
|
|
112
|
+
if source == "vault":
|
|
113
|
+
results.extend(self._search_vault(query, limit))
|
|
114
|
+
elif source == "zotero":
|
|
115
|
+
results.extend(self._search_zotero(query, limit))
|
|
116
|
+
elif source == "pdf":
|
|
117
|
+
results.extend(self._search_pdfs(query, limit))
|
|
118
|
+
|
|
119
|
+
# Sort by score (higher first)
|
|
120
|
+
results.sort(key=lambda x: x.score, reverse=True)
|
|
121
|
+
|
|
122
|
+
return results[:limit]
|
|
123
|
+
|
|
124
|
+
def _search_vault(self, query: str, limit: int) -> list[UnifiedSearchResult]:
|
|
125
|
+
"""Search vault notes."""
|
|
126
|
+
if not self._vault_manager:
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
vault_results = self._vault_manager.search(query, limit=limit)
|
|
130
|
+
results = []
|
|
131
|
+
|
|
132
|
+
for vr in vault_results:
|
|
133
|
+
# Calculate simple relevance score
|
|
134
|
+
score = 1.0
|
|
135
|
+
if vr.match_text.lower() == query.lower():
|
|
136
|
+
score = 2.0 # Exact match bonus
|
|
137
|
+
|
|
138
|
+
results.append(
|
|
139
|
+
UnifiedSearchResult(
|
|
140
|
+
source="vault",
|
|
141
|
+
path=vr.path,
|
|
142
|
+
title=Path(vr.path).stem.replace("-", " ").replace("_", " ").title(),
|
|
143
|
+
snippet=vr.content[:150] + "..." if len(vr.content) > 150 else vr.content,
|
|
144
|
+
score=score,
|
|
145
|
+
metadata={
|
|
146
|
+
"line_number": vr.line_number,
|
|
147
|
+
"match_text": vr.match_text,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return results
|
|
153
|
+
|
|
154
|
+
def _search_zotero(self, query: str, limit: int) -> list[UnifiedSearchResult]:
|
|
155
|
+
"""Search Zotero library."""
|
|
156
|
+
if not self.zotero_db or not self.zotero_db.exists():
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
from nexus.research.zotero import ZoteroClient
|
|
161
|
+
|
|
162
|
+
client = ZoteroClient(self.zotero_db)
|
|
163
|
+
items = client.search(query, limit=limit)
|
|
164
|
+
results = []
|
|
165
|
+
|
|
166
|
+
for item in items:
|
|
167
|
+
# Build author string
|
|
168
|
+
if item.authors:
|
|
169
|
+
if len(item.authors) > 2:
|
|
170
|
+
author_str = f"{item.authors[0]} et al."
|
|
171
|
+
else:
|
|
172
|
+
author_str = ", ".join(item.authors)
|
|
173
|
+
else:
|
|
174
|
+
author_str = "Unknown"
|
|
175
|
+
|
|
176
|
+
year = item.date[:4] if item.date else "n.d."
|
|
177
|
+
|
|
178
|
+
# Calculate score
|
|
179
|
+
score = 1.5 # Base score for Zotero (slightly higher than vault)
|
|
180
|
+
if query.lower() in item.title.lower():
|
|
181
|
+
score = 2.5 # Title match bonus
|
|
182
|
+
|
|
183
|
+
results.append(
|
|
184
|
+
UnifiedSearchResult(
|
|
185
|
+
source="zotero",
|
|
186
|
+
path=item.key,
|
|
187
|
+
title=item.title,
|
|
188
|
+
snippet=f"{author_str} ({year})",
|
|
189
|
+
score=score,
|
|
190
|
+
metadata={
|
|
191
|
+
"key": item.key,
|
|
192
|
+
"item_type": item.item_type,
|
|
193
|
+
"authors": item.authors,
|
|
194
|
+
"date": item.date,
|
|
195
|
+
"tags": item.tags,
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return results
|
|
201
|
+
except Exception:
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
def _search_pdfs(self, query: str, limit: int) -> list[UnifiedSearchResult]:
|
|
205
|
+
"""Search PDF content."""
|
|
206
|
+
if not self.pdf_dirs:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
from nexus.research.pdf import PDFExtractor
|
|
211
|
+
|
|
212
|
+
extractor = PDFExtractor(directories=self.pdf_dirs)
|
|
213
|
+
pdf_results = extractor.search(query, limit=limit)
|
|
214
|
+
results = []
|
|
215
|
+
|
|
216
|
+
for pr in pdf_results:
|
|
217
|
+
results.append(
|
|
218
|
+
UnifiedSearchResult(
|
|
219
|
+
source="pdf",
|
|
220
|
+
path=pr.path,
|
|
221
|
+
title=pr.filename,
|
|
222
|
+
snippet=pr.context[:150] + "..." if len(pr.context) > 150 else pr.context,
|
|
223
|
+
score=1.0,
|
|
224
|
+
metadata={
|
|
225
|
+
"page": pr.page,
|
|
226
|
+
"match_text": pr.match_text,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return results
|
|
232
|
+
except Exception:
|
|
233
|
+
return []
|