phorvec 0.1.2__cp38-abi3-win_amd64.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.
phorvec/__init__.py ADDED
@@ -0,0 +1,135 @@
1
+ """phorvec — persistent vector memory for AI agents (Python bindings).
2
+
3
+ The low-level engine (`Engine`) exposes every phorvec tool via
4
+ `Engine.call(name, params_json)`. On top of it, each feature family gets an
5
+ ergonomic wrapper class:
6
+
7
+ - `Memory` / `Recall` — durable agent memory (+ core-memory admin: pin, forget,
8
+ rewrite, prioritize, consolidate, export/import, gc, list, promote)
9
+ - `Team` — multi-agent teams (paid tier)
10
+ - `Graph` — knowledge graph (entities, links, queries, stats)
11
+ - `Facts` — temporal facts (supersede/invalidate/reactivate/history)
12
+ - `Skills` — skill library (ingest/search/get/list/sync)
13
+ - `Rag` — retrieval over indexed files/directories
14
+ - `MemoryBlocks` — shared, token-budgeted memory blocks
15
+ - `Context` — working-context store/retrieve + lifecycle ops
16
+ - `Snapshots` — context snapshots (save/restore/list/delete)
17
+ - `Branches` — context branches (create/switch/merge/…)
18
+ - `Watches` — semantic context watches
19
+ - `Agents` — agent versioning, diff/rollback, export/import, templates
20
+ - `Chat` — chat session search/resume
21
+ - `Projects` / `Outcomes` — project lifecycle + outcome tracking
22
+ - `Maintenance` / `Quantization` — buffer/cache/GC ops + vector quantization
23
+ - `Analytics` / `Security` / `Anchors` / `Export` — analytics/metrics, PII scan,
24
+ anchors, obsidian export
25
+
26
+ Every wrapper except `Memory` BORROWS an externally-created `Engine` and does
27
+ not own its lifecycle — construct one `Engine` (or a `Memory`) and layer the
28
+ others on top of the same handle.
29
+ """
30
+ from ._phorvec import (
31
+ Engine,
32
+ PhorvecError,
33
+ TierRequiredError,
34
+ UnknownToolError,
35
+ run_cli,
36
+ )
37
+ from .memory import Memory, Recall
38
+ from .team import Team
39
+ from .graph import Graph, Entity, GraphStats
40
+ from .facts import Facts, FactHistory, FactCurrentFact, FactEntry
41
+ from .skills import Skills, SkillEntry, SkillDetail, SkillHit
42
+ from .rag import Rag, RagHit
43
+ from .memory_blocks import MemoryBlocks, Block, BlockListResult
44
+ from .context import Context, StoreResult, RetrieveHit, TokenCount
45
+ from .snapshots import Snapshots, SnapshotInfo
46
+ from .branches import Branches, BranchInfo
47
+ from .watches import Watches, Watch, WatchInfo
48
+ from .agents import (
49
+ Agents,
50
+ VersionEntry,
51
+ AgentHistory,
52
+ DiffResult,
53
+ DiffStats,
54
+ Template,
55
+ TemplateVariable,
56
+ )
57
+ from .chat import Chat, SearchHit, SearchSession, Session, Turn, ResumeResult
58
+ from .projects import Projects, Outcomes, ProjectInfo, OutcomeEntry
59
+ from .maintenance import Maintenance, Quantization, BufferStatus
60
+ from .admin import (
61
+ Analytics,
62
+ Security,
63
+ Anchors,
64
+ Export,
65
+ DetectedSecret,
66
+ ScanResult,
67
+ Anchor,
68
+ )
69
+
70
+ __all__ = [
71
+ # core
72
+ "Engine",
73
+ "PhorvecError",
74
+ "TierRequiredError",
75
+ "UnknownToolError",
76
+ "run_cli",
77
+ # wrapper classes
78
+ "Memory",
79
+ "Recall",
80
+ "Team",
81
+ "Graph",
82
+ "Entity",
83
+ "GraphStats",
84
+ "Facts",
85
+ "FactHistory",
86
+ "FactCurrentFact",
87
+ "FactEntry",
88
+ "Skills",
89
+ "SkillEntry",
90
+ "SkillDetail",
91
+ "SkillHit",
92
+ "Rag",
93
+ "RagHit",
94
+ "MemoryBlocks",
95
+ "Block",
96
+ "BlockListResult",
97
+ "Context",
98
+ "StoreResult",
99
+ "RetrieveHit",
100
+ "TokenCount",
101
+ "Snapshots",
102
+ "SnapshotInfo",
103
+ "Branches",
104
+ "BranchInfo",
105
+ "Watches",
106
+ "Watch",
107
+ "WatchInfo",
108
+ "Agents",
109
+ "VersionEntry",
110
+ "AgentHistory",
111
+ "DiffResult",
112
+ "DiffStats",
113
+ "Template",
114
+ "TemplateVariable",
115
+ "Chat",
116
+ "SearchHit",
117
+ "SearchSession",
118
+ "Session",
119
+ "Turn",
120
+ "ResumeResult",
121
+ "Projects",
122
+ "Outcomes",
123
+ "ProjectInfo",
124
+ "OutcomeEntry",
125
+ "Maintenance",
126
+ "Quantization",
127
+ "BufferStatus",
128
+ "Analytics",
129
+ "Security",
130
+ "Anchors",
131
+ "Export",
132
+ "DetectedSecret",
133
+ "ScanResult",
134
+ "Anchor",
135
+ ]
phorvec/_cli.py ADDED
@@ -0,0 +1,7 @@
1
+ """Console-script entry: `phorvec ...` -> the Rust CLI via the extension."""
2
+ import sys
3
+ from ._phorvec import run_cli
4
+
5
+
6
+ def main() -> None:
7
+ raise SystemExit(run_cli(sys.argv))
phorvec/_phorvec.pyd ADDED
Binary file
phorvec/admin.py ADDED
@@ -0,0 +1,281 @@
1
+ """Admin tools for analytics, security scanning, anchor management, and vault export.
2
+
3
+ Wraps four singleton tool families: `analytics`/`metrics` (Analytics class),
4
+ `security_scan` (Security class), `anchor_manage` (Anchors class), and
5
+ `obsidian_export` (Export class). All four classes borrow an externally-created
6
+ Engine and do NOT own it — the caller controls the Engine lifecycle.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Optional
13
+
14
+ from ._phorvec import Engine
15
+
16
+
17
+ # ============================================================================
18
+ # Analytics — wraps `analytics` and `metrics` tools
19
+ # ============================================================================
20
+
21
+ class Analytics:
22
+ """Conversation analytics and token-savings metrics for a phorvec store.
23
+
24
+ `Analytics` BORROWS an externally-created `Engine` and does NOT own it,
25
+ so it has no `close()`/context-manager — the caller owns the Engine.
26
+ """
27
+
28
+ def __init__(self, engine: Engine, *, project: Optional[str] = None):
29
+ self._engine = engine
30
+ self._project = project
31
+
32
+ def _call(self, tool: str, params: dict) -> Any:
33
+ return json.loads(self._engine.call(tool, json.dumps(params)))
34
+
35
+ def query(
36
+ self,
37
+ *,
38
+ time_range: Optional[str] = None,
39
+ format: str = "json",
40
+ breakdown: Optional[bool] = None,
41
+ project: Optional[str] = None,
42
+ ) -> Any:
43
+ """Query conversation analytics (session stats, tool usage, topics, ROI).
44
+
45
+ Returns a dict when format='json' (default) or a str when format='human'.
46
+ time_range ∈ {'today','this_week','this_month','all_time'} (default: all_time).
47
+ breakdown=True gives a per-project breakdown instead of a single summary.
48
+ """
49
+ p = project or self._project
50
+ params: dict[str, Any] = {"format": format}
51
+ if p is not None:
52
+ params["project"] = p
53
+ if time_range is not None:
54
+ params["time_range"] = time_range
55
+ if breakdown is not None:
56
+ params["breakdown"] = breakdown
57
+ raw = self._engine.call("analytics", json.dumps(params))
58
+ return json.loads(raw) if format == "json" else raw
59
+
60
+ def get(
61
+ self,
62
+ *,
63
+ format: str = "json",
64
+ project: Optional[str] = None,
65
+ start_date: Optional[str] = None,
66
+ end_date: Optional[str] = None,
67
+ ) -> Any:
68
+ """Get token-savings metrics (cumulative, per-project, daily history).
69
+
70
+ Returns a dict when format='json' (default) or a str when format='human'.
71
+ Optionally filter by project and/or date range (YYYY-MM-DD strings).
72
+ """
73
+ p = project or self._project
74
+ params: dict[str, Any] = {"format": format}
75
+ if p is not None:
76
+ params["project"] = p
77
+ if start_date is not None:
78
+ params["start_date"] = start_date
79
+ if end_date is not None:
80
+ params["end_date"] = end_date
81
+ raw = self._engine.call("metrics", json.dumps(params))
82
+ return json.loads(raw) if format == "json" else raw
83
+
84
+
85
+ # ============================================================================
86
+ # Security — wraps `security_scan` tool
87
+ # ============================================================================
88
+
89
+ @dataclass
90
+ class DetectedSecret:
91
+ """One secret detected during a `security_scan`."""
92
+ type: str
93
+ start: int
94
+ end: int
95
+
96
+
97
+ @dataclass
98
+ class ScanResult:
99
+ """Full result of a `security_scan` call."""
100
+ secrets_found: int
101
+ action_taken: str
102
+ policy: str
103
+ clean_content: str
104
+ detected_secrets: list = field(default_factory=list)
105
+
106
+
107
+ class Security:
108
+ """Secret-scanning tool wrapper. Borrows an externally-created Engine."""
109
+
110
+ def __init__(self, engine: Engine):
111
+ self._engine = engine
112
+
113
+ def _call(self, tool: str, params: dict) -> Any:
114
+ return json.loads(self._engine.call(tool, json.dumps(params)))
115
+
116
+ def scan(self, content: str) -> ScanResult:
117
+ """Scan content for secrets (API keys, tokens, credentials) without storing it.
118
+
119
+ Requires a non-empty content string. Returns a ScanResult with all findings,
120
+ the action the configured security policy would take, and the redacted content.
121
+ Raises PhorvecError on engine errors; does NOT catch empty-content errors.
122
+ """
123
+ raw = self._call("security_scan", {"content": content})
124
+ secrets = [
125
+ DetectedSecret(type=s["type"], start=s["start"], end=s["end"])
126
+ for s in raw.get("detected_secrets", [])
127
+ ]
128
+ return ScanResult(
129
+ secrets_found=raw["secrets_found"],
130
+ action_taken=raw["action_taken"],
131
+ policy=raw["policy"],
132
+ clean_content=raw["clean_content"],
133
+ detected_secrets=secrets,
134
+ )
135
+
136
+
137
+ # ============================================================================
138
+ # Anchors — wraps `anchor_manage` tool
139
+ # ============================================================================
140
+
141
+ @dataclass
142
+ class Anchor:
143
+ """One semantic anchor returned by `anchor_manage` list."""
144
+ id: str
145
+ content: str
146
+ created_at: int
147
+ source: str
148
+ token_count: int
149
+
150
+
151
+ class Anchors:
152
+ """Manage semantic anchors — key facts that persist across context and resist pruning.
153
+
154
+ `Anchors` BORROWS an externally-created `Engine` and does NOT own it.
155
+ Maximum 20 anchors per project (enforced by the engine).
156
+ """
157
+
158
+ def __init__(self, engine: Engine, *, project: Optional[str] = None):
159
+ self._engine = engine
160
+ self._project = project
161
+
162
+ def _call(self, tool: str, params: dict) -> Any:
163
+ return json.loads(self._engine.call(tool, json.dumps(params)))
164
+
165
+ def _resolve_project(self, project: Optional[str]) -> str:
166
+ p = project or self._project
167
+ if p is None:
168
+ raise ValueError("project is required (pass project= or set it on the wrapper)")
169
+ return p
170
+
171
+ def manage(
172
+ self,
173
+ operation: str,
174
+ *,
175
+ content: Optional[str] = None,
176
+ anchor_id: Optional[str] = None,
177
+ project: Optional[str] = None,
178
+ ) -> dict:
179
+ """Generic anchor_manage call. Prefer the list/add/remove/update helpers.
180
+
181
+ operation ∈ {'list','add','remove','update'}.
182
+ content is required for add and update; anchor_id is required for remove and update.
183
+ Requires project (positional on wrapper, or per-call via project= kwarg).
184
+ """
185
+ p = self._resolve_project(project)
186
+ params: dict[str, Any] = {"project": p, "operation": operation}
187
+ if content is not None:
188
+ params["content"] = content
189
+ if anchor_id is not None:
190
+ params["anchor_id"] = anchor_id
191
+ return self._call("anchor_manage", params)
192
+
193
+ def list(self, *, project: Optional[str] = None) -> list:
194
+ """List all anchors for the project. Returns a list of Anchor objects."""
195
+ out = self.manage("list", project=project)
196
+ return [
197
+ Anchor(
198
+ id=a["id"],
199
+ content=a["content"],
200
+ created_at=a.get("created_at", 0),
201
+ source=a.get("source", "manual"),
202
+ token_count=a.get("token_count", 0),
203
+ )
204
+ for a in out.get("anchors", [])
205
+ ]
206
+
207
+ def add(self, content: str, *, project: Optional[str] = None) -> dict:
208
+ """Add a new anchor. Returns {id, content, importance, anchor_count, max_anchors}.
209
+
210
+ Requires content (non-empty string). Raises if the per-project limit (20) is reached.
211
+ """
212
+ return self.manage("add", content=content, project=project)
213
+
214
+ def remove(self, anchor_id: str, *, project: Optional[str] = None) -> dict:
215
+ """Remove the anchor flag from anchor_id. Returns {removed, message}.
216
+
217
+ The underlying context item remains; only the anchor designation is cleared.
218
+ Requires anchor_id.
219
+ """
220
+ return self.manage("remove", anchor_id=anchor_id, project=project)
221
+
222
+ def update(self, anchor_id: str, content: str, *, project: Optional[str] = None) -> dict:
223
+ """Update the content of an existing anchor. Returns {updated, content, message}.
224
+
225
+ Requires anchor_id (must point to an existing anchor) and new content.
226
+ """
227
+ return self.manage("update", content=content, anchor_id=anchor_id, project=project)
228
+
229
+
230
+ # ============================================================================
231
+ # Export — wraps `obsidian_export` tool
232
+ # ============================================================================
233
+
234
+ class Export:
235
+ """Obsidian vault export for phorvec projects.
236
+
237
+ `Export` BORROWS an externally-created `Engine` and does NOT own it.
238
+ The generated vault is a read-only projection; edits do not flow back.
239
+ """
240
+
241
+ def __init__(self, engine: Engine, *, project: Optional[str] = None):
242
+ self._engine = engine
243
+ self._project = project
244
+
245
+ def _call(self, tool: str, params: dict) -> Any:
246
+ return json.loads(self._engine.call(tool, json.dumps(params)))
247
+
248
+ def _resolve_project(self, project: Optional[str]) -> str:
249
+ p = project or self._project
250
+ if p is None:
251
+ raise ValueError("project is required (pass project= or set it on the wrapper)")
252
+ return p
253
+
254
+ def obsidian(
255
+ self,
256
+ *,
257
+ vault_root: Optional[str] = None,
258
+ force: Optional[bool] = None,
259
+ preserve_all: Optional[bool] = None,
260
+ dry_run: Optional[bool] = None,
261
+ project: Optional[str] = None,
262
+ ) -> dict:
263
+ """Export the project to an Obsidian markdown vault. Returns an export report dict.
264
+
265
+ Requires project. vault_root defaults to ~/phorvec/vault/<project>/.
266
+ force=True overwrites externally-edited vault files (you lose those edits).
267
+ preserve_all=True silently skips externally-edited files.
268
+ force and preserve_all are mutually exclusive.
269
+ dry_run=True previews what would happen without touching the filesystem.
270
+ """
271
+ p = self._resolve_project(project)
272
+ params: dict[str, Any] = {"project": p}
273
+ if vault_root is not None:
274
+ params["vault_root"] = vault_root
275
+ if force is not None:
276
+ params["force"] = force
277
+ if preserve_all is not None:
278
+ params["preserve_all"] = preserve_all
279
+ if dry_run is not None:
280
+ params["dry_run"] = dry_run
281
+ return self._call("obsidian_export", params)