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.
Files changed (130) hide show
  1. neuralcleave/__init__.py +8 -0
  2. neuralcleave/agent/__init__.py +1 -0
  3. neuralcleave/agent/pipeline.py +301 -0
  4. neuralcleave/agent/runtime.py +856 -0
  5. neuralcleave/agent/session.py +174 -0
  6. neuralcleave/autostart.py +377 -0
  7. neuralcleave/canvas/__init__.py +7 -0
  8. neuralcleave/canvas/block.py +72 -0
  9. neuralcleave/canvas/renderer.py +110 -0
  10. neuralcleave/canvas/routes.py +399 -0
  11. neuralcleave/canvas/tool.py +179 -0
  12. neuralcleave/channels/__init__.py +1 -0
  13. neuralcleave/channels/base.py +120 -0
  14. neuralcleave/channels/bluesky.py +356 -0
  15. neuralcleave/channels/discord_.py +146 -0
  16. neuralcleave/channels/email_.py +258 -0
  17. neuralcleave/channels/feishu.py +447 -0
  18. neuralcleave/channels/google_chat.py +339 -0
  19. neuralcleave/channels/imessage.py +282 -0
  20. neuralcleave/channels/irc.py +261 -0
  21. neuralcleave/channels/line.py +336 -0
  22. neuralcleave/channels/mastodon_.py +241 -0
  23. neuralcleave/channels/matrix.py +196 -0
  24. neuralcleave/channels/mattermost.py +235 -0
  25. neuralcleave/channels/messenger.py +303 -0
  26. neuralcleave/channels/nextcloud.py +297 -0
  27. neuralcleave/channels/nostr.py +598 -0
  28. neuralcleave/channels/qq_bot.py +457 -0
  29. neuralcleave/channels/rocketchat.py +326 -0
  30. neuralcleave/channels/signal_.py +221 -0
  31. neuralcleave/channels/slack.py +219 -0
  32. neuralcleave/channels/sms.py +176 -0
  33. neuralcleave/channels/synology_chat.py +339 -0
  34. neuralcleave/channels/teams.py +225 -0
  35. neuralcleave/channels/telegram.py +152 -0
  36. neuralcleave/channels/tlon.py +496 -0
  37. neuralcleave/channels/twilio_voice.py +496 -0
  38. neuralcleave/channels/twitch.py +374 -0
  39. neuralcleave/channels/viber.py +307 -0
  40. neuralcleave/channels/webhook.py +201 -0
  41. neuralcleave/channels/wechat_work.py +409 -0
  42. neuralcleave/channels/whatsapp.py +224 -0
  43. neuralcleave/channels/xmpp.py +267 -0
  44. neuralcleave/channels/zalo.py +362 -0
  45. neuralcleave/cli.py +1888 -0
  46. neuralcleave/cloud/__init__.py +35 -0
  47. neuralcleave/cloud/config.py +82 -0
  48. neuralcleave/cloud/health.py +106 -0
  49. neuralcleave/cloud/manifests.py +160 -0
  50. neuralcleave/commands/__init__.py +17 -0
  51. neuralcleave/commands/handler.py +204 -0
  52. neuralcleave/config.py +200 -0
  53. neuralcleave/desktop_launcher.py +74 -0
  54. neuralcleave/gateway/__init__.py +1 -0
  55. neuralcleave/gateway/main.py +161 -0
  56. neuralcleave/gateway/routes.py +955 -0
  57. neuralcleave/gateway/terminal.py +161 -0
  58. neuralcleave/gateway/websocket.py +216 -0
  59. neuralcleave/hub/__init__.py +16 -0
  60. neuralcleave/hub/installer.py +253 -0
  61. neuralcleave/hub/package.py +76 -0
  62. neuralcleave/hub/registry.py +149 -0
  63. neuralcleave/hub/scanner.py +162 -0
  64. neuralcleave/init_wizard.py +291 -0
  65. neuralcleave/memory/__init__.py +1 -0
  66. neuralcleave/memory/archiver.py +119 -0
  67. neuralcleave/memory/compactor.py +191 -0
  68. neuralcleave/memory/long_term.py +410 -0
  69. neuralcleave/memory/retrieval.py +364 -0
  70. neuralcleave/memory/short_term.py +190 -0
  71. neuralcleave/memory/tagging.py +101 -0
  72. neuralcleave/models/__init__.py +1 -0
  73. neuralcleave/models/deepseek.py +180 -0
  74. neuralcleave/models/openai_.py +157 -0
  75. neuralcleave/models/router.py +1337 -0
  76. neuralcleave/observability/__init__.py +1 -0
  77. neuralcleave/observability/logs.py +161 -0
  78. neuralcleave/observability/metrics.py +338 -0
  79. neuralcleave/orchestrator/__init__.py +42 -0
  80. neuralcleave/orchestrator/memory.py +229 -0
  81. neuralcleave/orchestrator/node.py +202 -0
  82. neuralcleave/orchestrator/orchestrator.py +242 -0
  83. neuralcleave/orchestrator/task.py +93 -0
  84. neuralcleave/plugins/__init__.py +1 -0
  85. neuralcleave/plugins/base.py +101 -0
  86. neuralcleave/plugins/registry.py +260 -0
  87. neuralcleave/pwa/__init__.py +1 -0
  88. neuralcleave/pwa/manifest.py +74 -0
  89. neuralcleave/pwa/push.py +174 -0
  90. neuralcleave/pwa/routes.py +443 -0
  91. neuralcleave/reflection/__init__.py +1 -0
  92. neuralcleave/reflection/engine.py +218 -0
  93. neuralcleave/sandbox/__init__.py +43 -0
  94. neuralcleave/sandbox/base.py +81 -0
  95. neuralcleave/sandbox/docker.py +196 -0
  96. neuralcleave/sandbox/local.py +160 -0
  97. neuralcleave/sandbox/manager.py +191 -0
  98. neuralcleave/sandbox/ssh.py +226 -0
  99. neuralcleave/scheduler.py +323 -0
  100. neuralcleave/skills/__init__.py +11 -0
  101. neuralcleave/skills/dynamic.py +128 -0
  102. neuralcleave/skills/writer.py +333 -0
  103. neuralcleave/tools/__init__.py +1 -0
  104. neuralcleave/tools/base.py +114 -0
  105. neuralcleave/tools/browser.py +503 -0
  106. neuralcleave/tools/file_ops.py +415 -0
  107. neuralcleave/tools/registry.py +162 -0
  108. neuralcleave/tools/shell.py +305 -0
  109. neuralcleave/tools/web_search.py +140 -0
  110. neuralcleave/tools/write_skill_tool.py +130 -0
  111. neuralcleave/update_checker.py +58 -0
  112. neuralcleave/voice/__init__.py +1 -0
  113. neuralcleave/voice/continuous.py +324 -0
  114. neuralcleave/voice/stt.py +106 -0
  115. neuralcleave/voice/tts.py +230 -0
  116. neuralcleave/voice/wake_word.py +216 -0
  117. neuralcleave/workspace.py +158 -0
  118. neuralcleave-2.1.0.dist-info/METADATA +759 -0
  119. neuralcleave-2.1.0.dist-info/RECORD +130 -0
  120. neuralcleave-2.1.0.dist-info/WHEEL +5 -0
  121. neuralcleave-2.1.0.dist-info/entry_points.txt +3 -0
  122. neuralcleave-2.1.0.dist-info/licenses/LICENSE +105 -0
  123. neuralcleave-2.1.0.dist-info/top_level.txt +2 -0
  124. neuralcleave-sdk/src/neuralcleave_sdk/__init__.py +38 -0
  125. neuralcleave-sdk/src/neuralcleave_sdk/channels.py +99 -0
  126. neuralcleave-sdk/src/neuralcleave_sdk/plugins.py +101 -0
  127. neuralcleave-sdk/src/neuralcleave_sdk/tools.py +116 -0
  128. neuralcleave-sdk/tests/test_channels.py +92 -0
  129. neuralcleave-sdk/tests/test_plugins.py +90 -0
  130. neuralcleave-sdk/tests/test_tools.py +73 -0
@@ -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"