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.
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 []