nucleus-mcp 1.0.7 → 1.2.0
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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Nucleus Agent Runtime - Neural Triggers
|
|
3
|
-
========================================
|
|
4
|
-
The routing layer that matches events to agents.
|
|
5
|
-
Events trigger agents based on configurable rules.
|
|
6
|
-
|
|
7
|
-
Location: mcp_server_nucleus/runtime/triggers.py
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Dict, List, Optional
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_triggers_path(brain_path: Path) -> Path:
|
|
16
|
-
"""Get path to triggers configuration"""
|
|
17
|
-
return brain_path / "ledger" / "triggers.json"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def load_triggers(brain_path: Path) -> Dict:
|
|
21
|
-
"""Load trigger definitions from disk"""
|
|
22
|
-
triggers_path = get_triggers_path(brain_path)
|
|
23
|
-
|
|
24
|
-
if triggers_path.exists():
|
|
25
|
-
return json.loads(triggers_path.read_text())
|
|
26
|
-
|
|
27
|
-
# Return default triggers
|
|
28
|
-
return get_default_triggers()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_default_triggers() -> Dict:
|
|
32
|
-
"""
|
|
33
|
-
Default trigger definitions based on .brain/agents/*.md protocols.
|
|
34
|
-
Maps events to the agents that should handle them.
|
|
35
|
-
"""
|
|
36
|
-
return {
|
|
37
|
-
"version": "1.0",
|
|
38
|
-
"triggers": [
|
|
39
|
-
# Task Assignment (any agent)
|
|
40
|
-
{
|
|
41
|
-
"id": "task-assigned",
|
|
42
|
-
"event_type": "task_assigned",
|
|
43
|
-
"condition": "always",
|
|
44
|
-
"activates": "{{payload.target_agent}}", # Dynamic from payload
|
|
45
|
-
"description": "Route task to specified agent"
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
# Implementation → Critic Review
|
|
49
|
-
{
|
|
50
|
-
"id": "impl-complete-review",
|
|
51
|
-
"event_type": "implementation_complete",
|
|
52
|
-
"condition": "always",
|
|
53
|
-
"activates": "critic",
|
|
54
|
-
"description": "Developer finished, Critic reviews"
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
# Strategy Update → Critic Validation
|
|
58
|
-
{
|
|
59
|
-
"id": "strategy-validation",
|
|
60
|
-
"event_type": "strategy_updated",
|
|
61
|
-
"condition": "always",
|
|
62
|
-
"activates": "critic",
|
|
63
|
-
"description": "Strategy changed, Critic validates coherence"
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
# Spec Ready → Developer Implementation
|
|
67
|
-
{
|
|
68
|
-
"id": "spec-to-dev",
|
|
69
|
-
"event_type": "spec_ready_for_development",
|
|
70
|
-
"condition": "always",
|
|
71
|
-
"activates": "developer",
|
|
72
|
-
"description": "Architect finished spec, Developer implements"
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
# Review Approved → DevOps Deploy
|
|
76
|
-
{
|
|
77
|
-
"id": "approved-to-deploy",
|
|
78
|
-
"event_type": "review_approved",
|
|
79
|
-
"condition": "payload.review_type == 'code'",
|
|
80
|
-
"activates": "devops",
|
|
81
|
-
"description": "Code approved, DevOps can deploy"
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
# Critical Events → Synthesizer Escalation
|
|
85
|
-
{
|
|
86
|
-
"id": "critical-escalation",
|
|
87
|
-
"event_type": "*",
|
|
88
|
-
"condition": "severity == 'CRITICAL'",
|
|
89
|
-
"activates": "synthesizer",
|
|
90
|
-
"description": "Any critical event escalates to Synthesizer"
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
# Sprint Started → Research Kick-off
|
|
94
|
-
{
|
|
95
|
-
"id": "sprint-research",
|
|
96
|
-
"event_type": "sprint_started",
|
|
97
|
-
"condition": "always",
|
|
98
|
-
"activates": "researcher",
|
|
99
|
-
"description": "New sprint, Researcher gathers context"
|
|
100
|
-
}
|
|
101
|
-
]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def save_triggers(brain_path: Path, triggers: Dict) -> None:
|
|
106
|
-
"""Save trigger definitions to disk"""
|
|
107
|
-
triggers_path = get_triggers_path(brain_path)
|
|
108
|
-
triggers_path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
-
triggers_path.write_text(json.dumps(triggers, indent=2))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def evaluate_condition(condition: str, event: Dict) -> bool:
|
|
113
|
-
"""
|
|
114
|
-
Evaluate a trigger condition against an event.
|
|
115
|
-
Supports:
|
|
116
|
-
- "always" - always matches
|
|
117
|
-
- "severity == 'CRITICAL'" - severity check
|
|
118
|
-
- "payload.field == 'value'" - payload field check
|
|
119
|
-
"""
|
|
120
|
-
if condition == "always":
|
|
121
|
-
return True
|
|
122
|
-
|
|
123
|
-
# Simple expression evaluation (safe subset)
|
|
124
|
-
try:
|
|
125
|
-
# Extract variables from event
|
|
126
|
-
severity = event.get("severity", "ROUTINE")
|
|
127
|
-
payload = event.get("payload", {})
|
|
128
|
-
event_type = event.get("event_type", "")
|
|
129
|
-
|
|
130
|
-
# Basic condition parsing
|
|
131
|
-
if "severity ==" in condition:
|
|
132
|
-
target = condition.split("==")[1].strip().strip("'\"")
|
|
133
|
-
return severity == target
|
|
134
|
-
|
|
135
|
-
if "payload." in condition:
|
|
136
|
-
# e.g., "payload.review_type == 'code'"
|
|
137
|
-
parts = condition.split("==")
|
|
138
|
-
field_path = parts[0].strip().replace("payload.", "")
|
|
139
|
-
target = parts[1].strip().strip("'\"")
|
|
140
|
-
return payload.get(field_path) == target
|
|
141
|
-
|
|
142
|
-
if "event_type ==" in condition:
|
|
143
|
-
target = condition.split("==")[1].strip().strip("'\"")
|
|
144
|
-
return event_type == target
|
|
145
|
-
|
|
146
|
-
except Exception:
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
return False
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def resolve_agent(activates: str, event: Dict) -> Optional[str]:
|
|
153
|
-
"""
|
|
154
|
-
Resolve the agent name from the trigger.
|
|
155
|
-
Supports dynamic references like {{payload.target_agent}}
|
|
156
|
-
"""
|
|
157
|
-
if activates.startswith("{{") and activates.endswith("}}"):
|
|
158
|
-
# Dynamic reference
|
|
159
|
-
path = activates[2:-2].strip()
|
|
160
|
-
|
|
161
|
-
if path.startswith("payload."):
|
|
162
|
-
field = path.replace("payload.", "")
|
|
163
|
-
return event.get("payload", {}).get(field)
|
|
164
|
-
|
|
165
|
-
return event.get(path)
|
|
166
|
-
|
|
167
|
-
# Static agent name
|
|
168
|
-
return activates
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def match_triggers(brain_path: Path, event: Dict) -> List[Dict]:
|
|
172
|
-
"""
|
|
173
|
-
Find all triggers that match an event.
|
|
174
|
-
Returns list of matched triggers with resolved agent names.
|
|
175
|
-
"""
|
|
176
|
-
triggers_config = load_triggers(brain_path)
|
|
177
|
-
triggers = triggers_config.get("triggers", [])
|
|
178
|
-
matched = []
|
|
179
|
-
|
|
180
|
-
event_type = event.get("event_type") or event.get("type", "")
|
|
181
|
-
|
|
182
|
-
for trigger in triggers:
|
|
183
|
-
trigger_event_type = trigger.get("event_type", "")
|
|
184
|
-
|
|
185
|
-
# Check event type match
|
|
186
|
-
if trigger_event_type != "*" and trigger_event_type != event_type:
|
|
187
|
-
continue
|
|
188
|
-
|
|
189
|
-
# Check condition
|
|
190
|
-
condition = trigger.get("condition", "always")
|
|
191
|
-
if not evaluate_condition(condition, event):
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
|
-
# Resolve agent
|
|
195
|
-
agent = resolve_agent(trigger.get("activates", ""), event)
|
|
196
|
-
if not agent:
|
|
197
|
-
continue
|
|
198
|
-
|
|
199
|
-
matched.append({
|
|
200
|
-
"trigger_id": trigger.get("id"),
|
|
201
|
-
"agent": agent,
|
|
202
|
-
"description": trigger.get("description", ""),
|
|
203
|
-
"event": event
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
return matched
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def get_agents_for_event(brain_path: Path, event: Dict) -> List[str]:
|
|
210
|
-
"""
|
|
211
|
-
Simple helper: get list of agent names that should be activated for an event.
|
|
212
|
-
"""
|
|
213
|
-
matched = match_triggers(brain_path, event)
|
|
214
|
-
return list(set(m["agent"] for m in matched))
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def add_trigger(brain_path: Path, trigger: Dict) -> None:
|
|
218
|
-
"""Add a new trigger definition"""
|
|
219
|
-
triggers_config = load_triggers(brain_path)
|
|
220
|
-
triggers_config["triggers"].append(trigger)
|
|
221
|
-
save_triggers(brain_path, triggers_config)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def remove_trigger(brain_path: Path, trigger_id: str) -> bool:
|
|
225
|
-
"""Remove a trigger by ID"""
|
|
226
|
-
triggers_config = load_triggers(brain_path)
|
|
227
|
-
original_count = len(triggers_config["triggers"])
|
|
228
|
-
triggers_config["triggers"] = [
|
|
229
|
-
t for t in triggers_config["triggers"]
|
|
230
|
-
if t.get("id") != trigger_id
|
|
231
|
-
]
|
|
232
|
-
|
|
233
|
-
if len(triggers_config["triggers"]) < original_count:
|
|
234
|
-
save_triggers(brain_path, triggers_config)
|
|
235
|
-
return True
|
|
236
|
-
|
|
237
|
-
return False
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import logging
|
|
3
|
-
import time
|
|
4
|
-
from typing import List, Dict, Any
|
|
5
|
-
from .llm_client import DualEngineLLM
|
|
6
|
-
from .storage import get_firestore_client, STORAGE_TYPE
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger("nucleus.vector_store")
|
|
9
|
-
|
|
10
|
-
class VectorStore:
|
|
11
|
-
"""
|
|
12
|
-
Manages Vector Storage and Retrieval using Firestore and Gemini Embeddings.
|
|
13
|
-
"""
|
|
14
|
-
def __init__(self):
|
|
15
|
-
# We use a separate collection for memory vectors to avoid polluting the 'brain' file system
|
|
16
|
-
self.collection_name = "nucleus_memory"
|
|
17
|
-
self.enabled = STORAGE_TYPE == "firestore"
|
|
18
|
-
|
|
19
|
-
if self.enabled:
|
|
20
|
-
# Only initialize LLM if we are actually going to use it (requires API Key)
|
|
21
|
-
self.llm = DualEngineLLM(model_name="text-embedding-004")
|
|
22
|
-
else:
|
|
23
|
-
self.llm = None
|
|
24
|
-
logger.warning("⚠️ VectorStore disabled: NUCLEUS_STORAGE_TYPE is not 'firestore'.")
|
|
25
|
-
|
|
26
|
-
def store_memory(self, content: str, metadata: Dict[str, Any] = None) -> str:
|
|
27
|
-
"""
|
|
28
|
-
Embeds and stores a memory chunk.
|
|
29
|
-
Returns the Document ID.
|
|
30
|
-
"""
|
|
31
|
-
if not self.enabled:
|
|
32
|
-
logger.info(f"Skipping memory storage (Local Mode): {content[:30]}...")
|
|
33
|
-
return "local_mock_id"
|
|
34
|
-
|
|
35
|
-
metadata = metadata or {}
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
# Generate Embedding
|
|
39
|
-
embedding_resp = self.llm.embed_content(content, task_type="retrieval_document")
|
|
40
|
-
vector = embedding_resp.get('embedding', [])
|
|
41
|
-
|
|
42
|
-
if not vector:
|
|
43
|
-
raise ValueError("Failed to generate embedding")
|
|
44
|
-
|
|
45
|
-
# Store in Firestore
|
|
46
|
-
db = get_firestore_client()
|
|
47
|
-
doc_ref = db.collection(self.collection_name).document()
|
|
48
|
-
|
|
49
|
-
payload = {
|
|
50
|
-
"content": content,
|
|
51
|
-
"embedding": vector, # Firestore supports Vector types? Or just list of floats.
|
|
52
|
-
# Note: Pure Firestore vector search requires VectorValue.
|
|
53
|
-
# For MVP with `google-cloud-firestore` standard client, we store as raw list
|
|
54
|
-
# OR we need to use the specific Vector helper if available.
|
|
55
|
-
# Assuming standard list for now, checking underlying support.
|
|
56
|
-
# Wait, standard Firestore check:
|
|
57
|
-
# To use KNN, we need `Vector` class from `google.cloud.firestore_v1.vector`.
|
|
58
|
-
# Let's try to import it, or fallback.
|
|
59
|
-
"metadata": metadata,
|
|
60
|
-
"created_at": time.time()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
# Helper for Vector type if available
|
|
64
|
-
try:
|
|
65
|
-
from google.cloud.firestore_v1.vector import Vector
|
|
66
|
-
payload["embedding_vector"] = Vector(vector)
|
|
67
|
-
except ImportError:
|
|
68
|
-
# Fallback/Older lib: Just store list, but search won't work efficiently without it.
|
|
69
|
-
pass
|
|
70
|
-
|
|
71
|
-
doc_ref.set(payload)
|
|
72
|
-
logger.info(f"🧠 Stored memory {doc_ref.id}")
|
|
73
|
-
return doc_ref.id
|
|
74
|
-
|
|
75
|
-
except Exception as e:
|
|
76
|
-
logger.error(f"Failed to store memory: {e}")
|
|
77
|
-
raise
|
|
78
|
-
|
|
79
|
-
def search_memory(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
80
|
-
"""
|
|
81
|
-
Semantic search for memories.
|
|
82
|
-
"""
|
|
83
|
-
if not self.enabled:
|
|
84
|
-
return [{"content": "Memory disabled in local mode", "score": 0.0}]
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
# Embed Query
|
|
88
|
-
embedding_resp = self.llm.embed_content(query, task_type="retrieval_query")
|
|
89
|
-
query_vector = embedding_resp.get('embedding', [])
|
|
90
|
-
|
|
91
|
-
if not query_vector:
|
|
92
|
-
return []
|
|
93
|
-
|
|
94
|
-
db = get_firestore_client()
|
|
95
|
-
coll = db.collection(self.collection_name)
|
|
96
|
-
|
|
97
|
-
# KNN Search
|
|
98
|
-
# Requres `google-cloud-firestore>=2.14.0`
|
|
99
|
-
from google.cloud.firestore_v1.vector import Vector
|
|
100
|
-
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
|
|
101
|
-
|
|
102
|
-
# Execute Vector Search
|
|
103
|
-
vector_query = coll.find_nearest(
|
|
104
|
-
vector_field="embedding_vector",
|
|
105
|
-
query_vector=Vector(query_vector),
|
|
106
|
-
distance_measure=DistanceMeasure.COSINE,
|
|
107
|
-
limit=limit
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
results = []
|
|
111
|
-
docs = vector_query.get()
|
|
112
|
-
for doc in docs:
|
|
113
|
-
data = doc.to_dict()
|
|
114
|
-
results.append({
|
|
115
|
-
"id": doc.id,
|
|
116
|
-
"content": data.get("content"),
|
|
117
|
-
"metadata": data.get("metadata"),
|
|
118
|
-
"score": 0.0 # Firestore doesn't return score easily in v1, implying raw sort
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
return results
|
|
122
|
-
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.error(f"Memory search failed: {e}")
|
|
125
|
-
return []
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
Async File Watcher for Nucleus.
|
|
4
|
-
Provides event-driven monitoring of Brain changes (directives, tasks).
|
|
5
|
-
|
|
6
|
-
Strategic Role:
|
|
7
|
-
- Moves Daemon from "Sleep Loop" to "Event Driven" (where possible).
|
|
8
|
-
- Monitors `directives.md` for policy updates.
|
|
9
|
-
- Monitors `task.md` for new commands.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import logging
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Callable, List, Dict
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
class AsyncFileWatcher:
|
|
20
|
-
def __init__(self, watch_paths: List[Path], callback: Callable, polling_interval: float = 2.0):
|
|
21
|
-
self.watch_paths = watch_paths
|
|
22
|
-
self.callback = callback
|
|
23
|
-
self.polling_interval = polling_interval
|
|
24
|
-
self.running = False
|
|
25
|
-
self._last_mtimes: Dict[Path, float] = {}
|
|
26
|
-
|
|
27
|
-
# Try to import watchdog for native OS events, fallback to polling
|
|
28
|
-
self.use_watchdog = False
|
|
29
|
-
try:
|
|
30
|
-
from watchdog.observers import Observer
|
|
31
|
-
self.use_watchdog = True
|
|
32
|
-
self.observer = Observer()
|
|
33
|
-
self._handler = self._create_handler()
|
|
34
|
-
except ImportError:
|
|
35
|
-
logger.info("Watchdog not installed. Falling back to Polling.")
|
|
36
|
-
|
|
37
|
-
def _create_handler(self):
|
|
38
|
-
from watchdog.events import FileSystemEventHandler
|
|
39
|
-
|
|
40
|
-
class Handler(FileSystemEventHandler):
|
|
41
|
-
def __init__(self, callback):
|
|
42
|
-
self.cb = callback
|
|
43
|
-
|
|
44
|
-
def on_modified(self, event):
|
|
45
|
-
if not event.is_directory:
|
|
46
|
-
# Debounce or just fire?
|
|
47
|
-
# For V1, just fire. simple.
|
|
48
|
-
asyncio.run_coroutine_threadsafe(self.cb(Path(event.src_path)), asyncio.get_running_loop())
|
|
49
|
-
|
|
50
|
-
return Handler(self.callback)
|
|
51
|
-
|
|
52
|
-
async def start(self):
|
|
53
|
-
"""Start the watcher"""
|
|
54
|
-
self.running = True
|
|
55
|
-
|
|
56
|
-
if self.use_watchdog:
|
|
57
|
-
logger.info("👀 Starting Native File Watcher (Watchdog)")
|
|
58
|
-
# Re-import to be safe or use what we confirmed
|
|
59
|
-
|
|
60
|
-
# Watch valid directories
|
|
61
|
-
watched_dirs = set()
|
|
62
|
-
for p in self.watch_paths:
|
|
63
|
-
if p.exists():
|
|
64
|
-
p_dir = p if p.is_dir() else p.parent
|
|
65
|
-
if p_dir not in watched_dirs:
|
|
66
|
-
self.observer.schedule(self._handler, str(p_dir), recursive=False)
|
|
67
|
-
watched_dirs.add(p_dir)
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
self.observer.start()
|
|
71
|
-
# Watchdog runs in thread, we just await nothing or keep loop alive elsewhere
|
|
72
|
-
except Exception as e:
|
|
73
|
-
logger.error(f"Watchdog failed to start: {e}. Fallback to polling.")
|
|
74
|
-
self.use_watchdog = False
|
|
75
|
-
|
|
76
|
-
if not self.use_watchdog:
|
|
77
|
-
logger.info(f"👀 Starting Polling Watcher ({self.polling_interval}s)")
|
|
78
|
-
asyncio.create_task(self._poll_loop())
|
|
79
|
-
|
|
80
|
-
async def _poll_loop(self):
|
|
81
|
-
"""Robust polling loop"""
|
|
82
|
-
# Init mtimes
|
|
83
|
-
for p in self.watch_paths:
|
|
84
|
-
if p.exists():
|
|
85
|
-
self._last_mtimes[p] = p.stat().st_mtime
|
|
86
|
-
|
|
87
|
-
while self.running:
|
|
88
|
-
for p in self.watch_paths:
|
|
89
|
-
if p.exists():
|
|
90
|
-
try:
|
|
91
|
-
current_mtime = p.stat().st_mtime
|
|
92
|
-
last_mtime = self._last_mtimes.get(p)
|
|
93
|
-
|
|
94
|
-
if last_mtime != current_mtime:
|
|
95
|
-
self._last_mtimes[p] = current_mtime
|
|
96
|
-
# Trigger callback
|
|
97
|
-
logger.debug(f"File changed: {p.name}")
|
|
98
|
-
if asyncio.iscoroutinefunction(self.callback):
|
|
99
|
-
await self.callback(p)
|
|
100
|
-
else:
|
|
101
|
-
self.callback(p)
|
|
102
|
-
except OSError:
|
|
103
|
-
pass # File reading error, skip this tick
|
|
104
|
-
|
|
105
|
-
await asyncio.sleep(self.polling_interval)
|
|
106
|
-
|
|
107
|
-
async def stop(self):
|
|
108
|
-
"""Stop watching"""
|
|
109
|
-
self.running = False
|
|
110
|
-
if self.use_watchdog:
|
|
111
|
-
self.observer.stop()
|
|
112
|
-
self.observer.join()
|