neuralcleave 2.1.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.
- neuralcleave/__init__.py +8 -0
- neuralcleave/agent/__init__.py +1 -0
- neuralcleave/agent/pipeline.py +301 -0
- neuralcleave/agent/runtime.py +856 -0
- neuralcleave/agent/session.py +174 -0
- neuralcleave/autostart.py +377 -0
- neuralcleave/canvas/__init__.py +7 -0
- neuralcleave/canvas/block.py +72 -0
- neuralcleave/canvas/renderer.py +110 -0
- neuralcleave/canvas/routes.py +399 -0
- neuralcleave/canvas/tool.py +179 -0
- neuralcleave/channels/__init__.py +1 -0
- neuralcleave/channels/base.py +120 -0
- neuralcleave/channels/bluesky.py +356 -0
- neuralcleave/channels/discord_.py +146 -0
- neuralcleave/channels/email_.py +258 -0
- neuralcleave/channels/feishu.py +447 -0
- neuralcleave/channels/google_chat.py +339 -0
- neuralcleave/channels/imessage.py +282 -0
- neuralcleave/channels/irc.py +261 -0
- neuralcleave/channels/line.py +336 -0
- neuralcleave/channels/mastodon_.py +241 -0
- neuralcleave/channels/matrix.py +196 -0
- neuralcleave/channels/mattermost.py +235 -0
- neuralcleave/channels/messenger.py +303 -0
- neuralcleave/channels/nextcloud.py +297 -0
- neuralcleave/channels/nostr.py +598 -0
- neuralcleave/channels/qq_bot.py +457 -0
- neuralcleave/channels/rocketchat.py +326 -0
- neuralcleave/channels/signal_.py +221 -0
- neuralcleave/channels/slack.py +219 -0
- neuralcleave/channels/sms.py +176 -0
- neuralcleave/channels/synology_chat.py +339 -0
- neuralcleave/channels/teams.py +225 -0
- neuralcleave/channels/telegram.py +152 -0
- neuralcleave/channels/tlon.py +496 -0
- neuralcleave/channels/twilio_voice.py +496 -0
- neuralcleave/channels/twitch.py +374 -0
- neuralcleave/channels/viber.py +307 -0
- neuralcleave/channels/webhook.py +201 -0
- neuralcleave/channels/wechat_work.py +409 -0
- neuralcleave/channels/whatsapp.py +224 -0
- neuralcleave/channels/xmpp.py +267 -0
- neuralcleave/channels/zalo.py +362 -0
- neuralcleave/cli.py +1888 -0
- neuralcleave/cloud/__init__.py +35 -0
- neuralcleave/cloud/config.py +82 -0
- neuralcleave/cloud/health.py +106 -0
- neuralcleave/cloud/manifests.py +160 -0
- neuralcleave/commands/__init__.py +17 -0
- neuralcleave/commands/handler.py +204 -0
- neuralcleave/config.py +200 -0
- neuralcleave/desktop_launcher.py +74 -0
- neuralcleave/gateway/__init__.py +1 -0
- neuralcleave/gateway/main.py +161 -0
- neuralcleave/gateway/routes.py +955 -0
- neuralcleave/gateway/terminal.py +161 -0
- neuralcleave/gateway/websocket.py +216 -0
- neuralcleave/hub/__init__.py +16 -0
- neuralcleave/hub/installer.py +253 -0
- neuralcleave/hub/package.py +76 -0
- neuralcleave/hub/registry.py +149 -0
- neuralcleave/hub/scanner.py +162 -0
- neuralcleave/init_wizard.py +291 -0
- neuralcleave/memory/__init__.py +1 -0
- neuralcleave/memory/archiver.py +119 -0
- neuralcleave/memory/compactor.py +191 -0
- neuralcleave/memory/long_term.py +410 -0
- neuralcleave/memory/retrieval.py +364 -0
- neuralcleave/memory/short_term.py +190 -0
- neuralcleave/memory/tagging.py +101 -0
- neuralcleave/models/__init__.py +1 -0
- neuralcleave/models/deepseek.py +180 -0
- neuralcleave/models/openai_.py +157 -0
- neuralcleave/models/router.py +1337 -0
- neuralcleave/observability/__init__.py +1 -0
- neuralcleave/observability/logs.py +161 -0
- neuralcleave/observability/metrics.py +338 -0
- neuralcleave/orchestrator/__init__.py +42 -0
- neuralcleave/orchestrator/memory.py +229 -0
- neuralcleave/orchestrator/node.py +202 -0
- neuralcleave/orchestrator/orchestrator.py +242 -0
- neuralcleave/orchestrator/task.py +93 -0
- neuralcleave/plugins/__init__.py +1 -0
- neuralcleave/plugins/base.py +101 -0
- neuralcleave/plugins/registry.py +260 -0
- neuralcleave/pwa/__init__.py +1 -0
- neuralcleave/pwa/manifest.py +74 -0
- neuralcleave/pwa/push.py +174 -0
- neuralcleave/pwa/routes.py +443 -0
- neuralcleave/reflection/__init__.py +1 -0
- neuralcleave/reflection/engine.py +218 -0
- neuralcleave/sandbox/__init__.py +43 -0
- neuralcleave/sandbox/base.py +81 -0
- neuralcleave/sandbox/docker.py +196 -0
- neuralcleave/sandbox/local.py +160 -0
- neuralcleave/sandbox/manager.py +191 -0
- neuralcleave/sandbox/ssh.py +226 -0
- neuralcleave/scheduler.py +323 -0
- neuralcleave/skills/__init__.py +11 -0
- neuralcleave/skills/dynamic.py +128 -0
- neuralcleave/skills/writer.py +333 -0
- neuralcleave/tools/__init__.py +1 -0
- neuralcleave/tools/base.py +114 -0
- neuralcleave/tools/browser.py +503 -0
- neuralcleave/tools/file_ops.py +415 -0
- neuralcleave/tools/registry.py +162 -0
- neuralcleave/tools/shell.py +305 -0
- neuralcleave/tools/web_search.py +140 -0
- neuralcleave/tools/write_skill_tool.py +130 -0
- neuralcleave/update_checker.py +58 -0
- neuralcleave/voice/__init__.py +1 -0
- neuralcleave/voice/continuous.py +324 -0
- neuralcleave/voice/stt.py +106 -0
- neuralcleave/voice/tts.py +230 -0
- neuralcleave/voice/wake_word.py +216 -0
- neuralcleave/workspace.py +158 -0
- neuralcleave-2.1.0.dist-info/METADATA +759 -0
- neuralcleave-2.1.0.dist-info/RECORD +130 -0
- neuralcleave-2.1.0.dist-info/WHEEL +5 -0
- neuralcleave-2.1.0.dist-info/entry_points.txt +3 -0
- neuralcleave-2.1.0.dist-info/licenses/LICENSE +105 -0
- neuralcleave-2.1.0.dist-info/top_level.txt +2 -0
- neuralcleave-sdk/src/neuralcleave_sdk/__init__.py +38 -0
- neuralcleave-sdk/src/neuralcleave_sdk/channels.py +99 -0
- neuralcleave-sdk/src/neuralcleave_sdk/plugins.py +101 -0
- neuralcleave-sdk/src/neuralcleave_sdk/tools.py +116 -0
- neuralcleave-sdk/tests/test_channels.py +92 -0
- neuralcleave-sdk/tests/test_plugins.py +90 -0
- neuralcleave-sdk/tests/test_tools.py +73 -0
neuralcleave/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""NeuralCleave v2 — Personal AI Assistant Gateway.
|
|
2
|
+
|
|
3
|
+
A local-first AI assistant that connects to all major messaging platforms
|
|
4
|
+
with 3-tier hierarchical memory, task-aware LLM routing, and cross-platform voice.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "2.1.0"
|
|
8
|
+
__author__ = "Amit Chandra"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent runtime: session management, cognitive pipeline, and main loop."""
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Cognitive pipeline: intent extraction → memory retrieval → generation → reflection.
|
|
2
|
+
|
|
3
|
+
The pipeline is the heart of NeuralCleave's intelligence layer. Each inbound
|
|
4
|
+
message passes through these stages:
|
|
5
|
+
|
|
6
|
+
1. Intent extraction — classify what the user wants (Gemini Flash, cheap)
|
|
7
|
+
2. Memory retrieval — assemble context from 3-tier memory
|
|
8
|
+
3. Prompt assembly — workspace system prompt + memory + conversation
|
|
9
|
+
4. Generation — route to optimal model via ModelRouter
|
|
10
|
+
5. Reflection — quality-score the response (async, non-blocking)
|
|
11
|
+
6. Memory storage — persist to short-term Redis and trigger long-term write
|
|
12
|
+
|
|
13
|
+
Stages 1–5 are synchronous within the request path.
|
|
14
|
+
Stage 6 is fire-and-forget (asyncio.create_task).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import AsyncIterator
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
from neuralcleave.agent.session import Session
|
|
26
|
+
from neuralcleave.channels.base import InboundMessage
|
|
27
|
+
from neuralcleave.memory.retrieval import MemoryRetrievalPipeline, RetrievalContext
|
|
28
|
+
from neuralcleave.models.router import GenerationResult, ModelRouter
|
|
29
|
+
from neuralcleave.reflection.engine import ReflectionEngine
|
|
30
|
+
from neuralcleave.workspace import WorkspaceFiles
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Intent labels understood by the pipeline
|
|
35
|
+
INTENT_TASK_MAP: dict[str, str] = {
|
|
36
|
+
"code": "code_generation",
|
|
37
|
+
"debug": "code_review",
|
|
38
|
+
"explain": "summarization",
|
|
39
|
+
"summarize": "summarization",
|
|
40
|
+
"plan": "task_decomposition",
|
|
41
|
+
"write": "general",
|
|
42
|
+
"question": "general",
|
|
43
|
+
"chat": "general",
|
|
44
|
+
"other": "general",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class PipelineResult:
|
|
50
|
+
"""Output of one pipeline pass."""
|
|
51
|
+
|
|
52
|
+
response: str
|
|
53
|
+
model: str
|
|
54
|
+
provider: str
|
|
55
|
+
intent: str
|
|
56
|
+
task_type: str
|
|
57
|
+
quality_score: float | None = None # filled in by reflection (async)
|
|
58
|
+
retrieval_token_estimate: int = 0
|
|
59
|
+
latency_ms: float = 0.0
|
|
60
|
+
usage: dict[str, int] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class PipelineStreamChunk:
|
|
65
|
+
"""One increment of a streaming pipeline pass.
|
|
66
|
+
|
|
67
|
+
Mirrors models.router.StreamChunk's shape, but the final (done=True)
|
|
68
|
+
chunk carries a full PipelineResult instead of bare model/provider/usage
|
|
69
|
+
fields, since the streaming path still runs intent extraction, memory
|
|
70
|
+
retrieval, and reflection around the one part that's actually streamed
|
|
71
|
+
(generation).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
text: str = ""
|
|
75
|
+
done: bool = False
|
|
76
|
+
error: str | None = None
|
|
77
|
+
result: PipelineResult | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CognitivePipeline:
|
|
81
|
+
"""Executes the full intent → memory → generate → reflect loop.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
router: LLM router for generation and intent extraction.
|
|
85
|
+
memory: Memory retrieval pipeline (3-tier).
|
|
86
|
+
workspace: Loaded workspace files (SOUL/TOOLS/RULES).
|
|
87
|
+
agent_name: Name of the assistant (used in system prompt).
|
|
88
|
+
reflection: Optional reflection engine. When provided, each response is
|
|
89
|
+
quality-scored inline (and self-corrected if below the
|
|
90
|
+
engine's threshold) before being returned. When None
|
|
91
|
+
(default), reflection is skipped and quality_score is None.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
router: ModelRouter,
|
|
97
|
+
memory: MemoryRetrievalPipeline,
|
|
98
|
+
workspace: WorkspaceFiles,
|
|
99
|
+
agent_name: str = "NeuralCleave",
|
|
100
|
+
reflection: ReflectionEngine | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self._router = router
|
|
103
|
+
self._memory = memory
|
|
104
|
+
self._workspace = workspace
|
|
105
|
+
self._agent_name = agent_name
|
|
106
|
+
self._reflection = reflection
|
|
107
|
+
|
|
108
|
+
async def run(
|
|
109
|
+
self,
|
|
110
|
+
message: InboundMessage,
|
|
111
|
+
session: Session,
|
|
112
|
+
) -> PipelineResult:
|
|
113
|
+
"""Process one inbound message and return the assistant's response."""
|
|
114
|
+
t0 = time.monotonic()
|
|
115
|
+
text = message.text or ""
|
|
116
|
+
|
|
117
|
+
# ── Stage 1: Intent extraction ─────────────────────────────────
|
|
118
|
+
intent = await self._extract_intent(text)
|
|
119
|
+
task_type = INTENT_TASK_MAP.get(intent, "general")
|
|
120
|
+
logger.debug("pipeline.intent text=%r intent=%s task_type=%s", text[:60], intent, task_type)
|
|
121
|
+
|
|
122
|
+
# ── Stage 2: Memory retrieval ──────────────────────────────────
|
|
123
|
+
ctx = await self._memory.retrieve(text, top_k=8, session_id=session.session_id)
|
|
124
|
+
|
|
125
|
+
# ── Stage 3: Prompt assembly ────────────────────────────────────
|
|
126
|
+
system_prompt = self._build_system(ctx, session)
|
|
127
|
+
user_prompt = self._build_user(text, session)
|
|
128
|
+
|
|
129
|
+
# ── Stage 4: Generation ─────────────────────────────────────────
|
|
130
|
+
gen: GenerationResult = await self._router.generate(
|
|
131
|
+
user_prompt,
|
|
132
|
+
task_type=task_type,
|
|
133
|
+
system=system_prompt,
|
|
134
|
+
)
|
|
135
|
+
response_text = gen.text.strip()
|
|
136
|
+
|
|
137
|
+
# ── Stage 5: Reflection (optional, inline) ─────────────────────
|
|
138
|
+
quality_score: float | None = None
|
|
139
|
+
if self._reflection is not None:
|
|
140
|
+
try:
|
|
141
|
+
refl = await self._reflection.reflect(text, response_text)
|
|
142
|
+
response_text = refl.final_response
|
|
143
|
+
quality_score = refl.score
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
logger.debug("reflection failed (%s) — keeping original response", exc)
|
|
146
|
+
|
|
147
|
+
# ── Stage 6: Update session history ────────────────────────────
|
|
148
|
+
session.add_turn("user", text)
|
|
149
|
+
session.add_turn("assistant", response_text, model=gen.model)
|
|
150
|
+
|
|
151
|
+
# ── Stage 7: Persist short-term memory (fire-and-forget) ───────
|
|
152
|
+
asyncio.create_task(
|
|
153
|
+
self._memory.store_short_term(
|
|
154
|
+
key=f"turn:{session.turn_count}",
|
|
155
|
+
value={"user": text, "assistant": response_text},
|
|
156
|
+
session_id=session.session_id,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
latency = (time.monotonic() - t0) * 1000
|
|
161
|
+
return PipelineResult(
|
|
162
|
+
response=response_text,
|
|
163
|
+
model=gen.model,
|
|
164
|
+
provider=gen.provider,
|
|
165
|
+
intent=intent,
|
|
166
|
+
task_type=task_type,
|
|
167
|
+
quality_score=quality_score,
|
|
168
|
+
retrieval_token_estimate=ctx.token_estimate,
|
|
169
|
+
latency_ms=round(latency, 1),
|
|
170
|
+
usage=gen.usage,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def run_stream(
|
|
174
|
+
self,
|
|
175
|
+
message: InboundMessage,
|
|
176
|
+
session: Session,
|
|
177
|
+
) -> AsyncIterator[PipelineStreamChunk]:
|
|
178
|
+
"""Streaming counterpart to run().
|
|
179
|
+
|
|
180
|
+
Stages 1–3 (intent, memory retrieval, prompt assembly) run exactly
|
|
181
|
+
as in run() — only generation is actually streamed, since that's
|
|
182
|
+
the only stage with a meaningful per-token output. Yields text
|
|
183
|
+
chunks as they arrive, then one final done=True chunk carrying the
|
|
184
|
+
full PipelineResult.
|
|
185
|
+
|
|
186
|
+
Reflection is intentionally NOT self-correcting here, unlike run():
|
|
187
|
+
once text has streamed to the caller, there's no way to retract or
|
|
188
|
+
replace it, so reflection only contributes a quality_score for
|
|
189
|
+
observability/storage — never overrides response text.
|
|
190
|
+
"""
|
|
191
|
+
t0 = time.monotonic()
|
|
192
|
+
text = message.text or ""
|
|
193
|
+
|
|
194
|
+
intent = await self._extract_intent(text)
|
|
195
|
+
task_type = INTENT_TASK_MAP.get(intent, "general")
|
|
196
|
+
ctx = await self._memory.retrieve(text, top_k=8, session_id=session.session_id)
|
|
197
|
+
system_prompt = self._build_system(ctx, session)
|
|
198
|
+
user_prompt = self._build_user(text, session)
|
|
199
|
+
|
|
200
|
+
accumulated: list[str] = []
|
|
201
|
+
final_model = ""
|
|
202
|
+
final_provider = ""
|
|
203
|
+
final_usage: dict[str, int] = {}
|
|
204
|
+
stream_error: str | None = None
|
|
205
|
+
|
|
206
|
+
async for chunk in self._router.generate_stream(
|
|
207
|
+
user_prompt, task_type=task_type, system=system_prompt
|
|
208
|
+
):
|
|
209
|
+
if chunk.error:
|
|
210
|
+
stream_error = chunk.error
|
|
211
|
+
break
|
|
212
|
+
if chunk.text:
|
|
213
|
+
accumulated.append(chunk.text)
|
|
214
|
+
yield PipelineStreamChunk(text=chunk.text)
|
|
215
|
+
if chunk.done:
|
|
216
|
+
final_model = chunk.model or ""
|
|
217
|
+
final_provider = chunk.provider or ""
|
|
218
|
+
final_usage = chunk.usage
|
|
219
|
+
|
|
220
|
+
if stream_error:
|
|
221
|
+
yield PipelineStreamChunk(done=True, error=stream_error)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
response_text = "".join(accumulated).strip()
|
|
225
|
+
|
|
226
|
+
quality_score: float | None = None
|
|
227
|
+
if self._reflection is not None:
|
|
228
|
+
try:
|
|
229
|
+
refl = await self._reflection.reflect(text, response_text)
|
|
230
|
+
quality_score = refl.score
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
logger.debug("reflection failed (%s) — score omitted", exc)
|
|
233
|
+
|
|
234
|
+
session.add_turn("user", text)
|
|
235
|
+
session.add_turn("assistant", response_text, model=final_model)
|
|
236
|
+
|
|
237
|
+
asyncio.create_task(
|
|
238
|
+
self._memory.store_short_term(
|
|
239
|
+
key=f"turn:{session.turn_count}",
|
|
240
|
+
value={"user": text, "assistant": response_text},
|
|
241
|
+
session_id=session.session_id,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
latency = (time.monotonic() - t0) * 1000
|
|
246
|
+
result = PipelineResult(
|
|
247
|
+
response=response_text,
|
|
248
|
+
model=final_model,
|
|
249
|
+
provider=final_provider,
|
|
250
|
+
intent=intent,
|
|
251
|
+
task_type=task_type,
|
|
252
|
+
quality_score=quality_score,
|
|
253
|
+
retrieval_token_estimate=ctx.token_estimate,
|
|
254
|
+
latency_ms=round(latency, 1),
|
|
255
|
+
usage=final_usage,
|
|
256
|
+
)
|
|
257
|
+
yield PipelineStreamChunk(done=True, result=result)
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# Prompt builders
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def _build_system(self, ctx: RetrievalContext, session: Session) -> str:
|
|
264
|
+
parts: list[str] = [self._workspace.to_system_prompt(self._agent_name)]
|
|
265
|
+
|
|
266
|
+
memory_blocks = ctx.to_prompt_blocks()
|
|
267
|
+
if memory_blocks:
|
|
268
|
+
parts.append("# Relevant memory\n" + "\n\n".join(memory_blocks))
|
|
269
|
+
|
|
270
|
+
return "\n\n".join(parts)
|
|
271
|
+
|
|
272
|
+
def _build_user(self, text: str, session: Session) -> str:
|
|
273
|
+
history = session.build_prompt(include_turns=10)
|
|
274
|
+
if history:
|
|
275
|
+
return f"{history}\nUser: {text}"
|
|
276
|
+
return text
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
# Intent extraction
|
|
280
|
+
# ------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
async def _extract_intent(self, text: str) -> str:
|
|
283
|
+
if len(text) < 5:
|
|
284
|
+
return "chat"
|
|
285
|
+
try:
|
|
286
|
+
result = await self._router.generate(
|
|
287
|
+
f"""Classify this user message into ONE of these intents:
|
|
288
|
+
code, debug, explain, summarize, plan, write, question, chat, other
|
|
289
|
+
|
|
290
|
+
Message: {text[:500]}
|
|
291
|
+
|
|
292
|
+
Reply with ONLY the intent word, nothing else.""",
|
|
293
|
+
task_type="intent_extraction",
|
|
294
|
+
max_tokens=10,
|
|
295
|
+
temperature=0.0,
|
|
296
|
+
)
|
|
297
|
+
intent = result.text.strip().lower().split()[0]
|
|
298
|
+
return intent if intent in INTENT_TASK_MAP else "other"
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
logger.debug("intent_extraction failed (%s), using 'general'", exc)
|
|
301
|
+
return "other"
|