gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,485 @@
1
+ """MemU memory backend integration.
2
+
3
+ This backend wraps the MemU SDK (NevaMind-AI/memU via memu-py) to provide a
4
+ MemoryBackendProtocol-compliant interface. MemU offers structured memory
5
+ storage with semantic search and categorization.
6
+
7
+ Requires: pip install memu-py
8
+
9
+ Example:
10
+ from gobby.memory.backends import get_backend
11
+
12
+ backend = get_backend("memu", database_type="inmemory")
13
+ record = await backend.create("User prefers dark mode")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from datetime import UTC, datetime
20
+ from typing import TYPE_CHECKING, Any
21
+ from uuid import uuid4
22
+
23
+ from gobby.memory.protocol import (
24
+ MediaAttachment,
25
+ MemoryCapability,
26
+ MemoryQuery,
27
+ MemoryRecord,
28
+ )
29
+
30
+ if TYPE_CHECKING:
31
+ from memu.app.service import MemoryService
32
+
33
+
34
+ class MemUBackend:
35
+ """MemU-based memory backend.
36
+
37
+ Wraps the MemU SDK (memu-py) to provide MemoryBackendProtocol interface.
38
+ Supports structured memory storage with semantic search.
39
+
40
+ Args:
41
+ database_type: Database type - "inmemory", "sqlite", or "postgres"
42
+ database_url: Database URL (for sqlite/postgres)
43
+ llm_api_key: API key for LLM provider (OpenAI, etc.)
44
+ llm_base_url: Base URL for LLM API
45
+ embedding_api_key: API key for embedding provider
46
+ embedding_base_url: Base URL for embedding API
47
+ user_id: Default user ID for memories
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ database_type: str = "inmemory",
53
+ database_url: str | None = None,
54
+ llm_api_key: str | None = None,
55
+ llm_base_url: str | None = None,
56
+ embedding_api_key: str | None = None,
57
+ embedding_base_url: str | None = None,
58
+ user_id: str | None = None,
59
+ **kwargs: Any,
60
+ ):
61
+ """Initialize the MemU backend.
62
+
63
+ Args:
64
+ database_type: Database backend type
65
+ database_url: Connection URL for database
66
+ llm_api_key: LLM API key
67
+ llm_base_url: LLM API base URL
68
+ embedding_api_key: Embedding API key
69
+ embedding_base_url: Embedding API base URL
70
+ user_id: Default user ID for operations
71
+ **kwargs: Additional configuration
72
+ """
73
+ # Lazy import to avoid circular dependencies
74
+ from memu.app.service import MemoryService
75
+
76
+ # Build configuration
77
+ config: dict[str, Any] = {}
78
+
79
+ # Database configuration
80
+ if database_type == "inmemory":
81
+ config["database_config"] = {"type": "inmemory"}
82
+ elif database_type == "sqlite":
83
+ config["database_config"] = {
84
+ "type": "sqlite",
85
+ "url": database_url or "sqlite:///memu.db",
86
+ }
87
+ elif database_type == "postgres":
88
+ if not database_url:
89
+ raise ValueError(
90
+ "database_url is required when database_type is 'postgres'. "
91
+ "Please provide a valid PostgreSQL connection URL."
92
+ )
93
+ config["database_config"] = {
94
+ "type": "postgres",
95
+ "url": database_url,
96
+ }
97
+
98
+ # LLM configuration (optional - uses OpenAI by default)
99
+ if llm_api_key or llm_base_url:
100
+ config["llm_profiles"] = {
101
+ "default": {
102
+ "api_key": llm_api_key,
103
+ "base_url": llm_base_url,
104
+ }
105
+ }
106
+
107
+ # Embedding configuration (optional - uses OpenAI by default)
108
+ if embedding_api_key or embedding_base_url:
109
+ config["embedding_profiles"] = {
110
+ "default": {
111
+ "api_key": embedding_api_key,
112
+ "base_url": embedding_base_url,
113
+ }
114
+ }
115
+
116
+ self._service: MemoryService = MemoryService(**config)
117
+ self._default_user_id = user_id
118
+
119
+ def capabilities(self) -> set[MemoryCapability]:
120
+ """Return supported capabilities.
121
+
122
+ MemU supports semantic search and structured CRUD operations.
123
+ """
124
+ return {
125
+ # Basic CRUD
126
+ MemoryCapability.CREATE,
127
+ MemoryCapability.READ,
128
+ MemoryCapability.UPDATE,
129
+ MemoryCapability.DELETE,
130
+ # Search
131
+ MemoryCapability.SEARCH_SEMANTIC,
132
+ MemoryCapability.SEARCH,
133
+ # Advanced
134
+ MemoryCapability.LIST,
135
+ # MCP-aligned
136
+ MemoryCapability.REMEMBER,
137
+ MemoryCapability.RECALL,
138
+ MemoryCapability.FORGET,
139
+ }
140
+
141
+ async def create(
142
+ self,
143
+ content: str,
144
+ memory_type: str = "fact",
145
+ importance: float = 0.5,
146
+ project_id: str | None = None,
147
+ user_id: str | None = None,
148
+ tags: list[str] | None = None,
149
+ source_type: str | None = None,
150
+ source_session_id: str | None = None,
151
+ media: list[MediaAttachment] | None = None,
152
+ metadata: dict[str, Any] | None = None,
153
+ ) -> MemoryRecord:
154
+ """Create a new memory in MemU.
155
+
156
+ Args:
157
+ content: The memory content text
158
+ memory_type: Type of memory (mapped to MemU memory types)
159
+ importance: Importance score (stored in metadata)
160
+ project_id: Associated project ID
161
+ user_id: User ID (uses default if not provided)
162
+ tags: List of tags (used as categories)
163
+ source_type: Origin of memory
164
+ source_session_id: Session that created the memory
165
+ media: List of media attachments (stored in metadata)
166
+ metadata: Additional metadata
167
+
168
+ Returns:
169
+ The created MemoryRecord
170
+ """
171
+ effective_user_id = user_id or self._default_user_id or "default"
172
+
173
+ # Map memory_type to MemU MemoryType
174
+ memu_type = self._map_memory_type(memory_type)
175
+
176
+ # Use tags as categories, or create from memory_type
177
+ categories = tags if tags else [memory_type]
178
+
179
+ # Create user scope
180
+ user_scope = {"user_id": effective_user_id}
181
+ if project_id:
182
+ user_scope["project_id"] = project_id
183
+
184
+ # Create memory via MemU service (run in thread to avoid blocking event loop)
185
+ result = await asyncio.to_thread(
186
+ self._service.create_memory_item,
187
+ memory_type=memu_type,
188
+ memory_content=content,
189
+ memory_categories=categories,
190
+ user=user_scope,
191
+ )
192
+
193
+ # Extract memory ID from result
194
+ memory_id = result.get("id") or result.get("memory_id") or str(uuid4())
195
+
196
+ return MemoryRecord(
197
+ id=memory_id,
198
+ content=content,
199
+ created_at=datetime.now(UTC),
200
+ memory_type=memory_type,
201
+ importance=importance,
202
+ project_id=project_id,
203
+ user_id=effective_user_id,
204
+ tags=tags or [],
205
+ source_type=source_type,
206
+ source_session_id=source_session_id,
207
+ metadata=metadata or {},
208
+ )
209
+
210
+ async def get(self, memory_id: str) -> MemoryRecord | None:
211
+ """Retrieve a memory by ID from MemU.
212
+
213
+ Args:
214
+ memory_id: The memory ID to retrieve
215
+
216
+ Returns:
217
+ The MemoryRecord if found, None otherwise
218
+ """
219
+ # Try direct lookup first (O(1) if SDK supports it)
220
+ try:
221
+ # Run in thread to avoid blocking event loop
222
+ result = await asyncio.to_thread(self._service.get_memory_item, memory_id=memory_id)
223
+ if result:
224
+ return self._memu_to_record(result)
225
+ return None
226
+ except AttributeError:
227
+ # SDK may not have get_memory_item, fall back to list scan
228
+ pass
229
+ except Exception:
230
+ return None
231
+
232
+ # Fallback: list and filter (O(n))
233
+ try:
234
+ # Run in thread to avoid blocking event loop
235
+ result = await asyncio.to_thread(self._service.list_memory_items)
236
+ items = result.get("items", result.get("memories", []))
237
+
238
+ for item in items:
239
+ if item.get("id") == memory_id or item.get("memory_id") == memory_id:
240
+ return self._memu_to_record(item)
241
+
242
+ return None
243
+ except Exception:
244
+ return None
245
+
246
+ async def update(
247
+ self,
248
+ memory_id: str,
249
+ content: str | None = None,
250
+ importance: float | None = None,
251
+ tags: list[str] | None = None,
252
+ ) -> MemoryRecord:
253
+ """Update an existing memory in MemU.
254
+
255
+ Args:
256
+ memory_id: The memory ID to update
257
+ content: New content (optional)
258
+ importance: New importance score (optional, stored in metadata)
259
+ tags: New tags/categories (optional)
260
+
261
+ Returns:
262
+ The updated MemoryRecord
263
+
264
+ Raises:
265
+ ValueError: If memory not found
266
+ """
267
+ existing = await self.get(memory_id)
268
+ if not existing:
269
+ raise ValueError(f"Memory not found: {memory_id}")
270
+
271
+ # Build update kwargs
272
+ update_kwargs: dict[str, Any] = {"memory_id": memory_id}
273
+ if content is not None:
274
+ update_kwargs["memory_content"] = content
275
+ if tags is not None:
276
+ update_kwargs["memory_categories"] = tags
277
+
278
+ # Run in thread to avoid blocking event loop
279
+ result = await asyncio.to_thread(lambda: self._service.update_memory_item(**update_kwargs))
280
+
281
+ if result:
282
+ # Re-fetch to get updated record
283
+ updated = await self.get(memory_id)
284
+ if updated:
285
+ return updated
286
+
287
+ # Fallback: return synthetic updated record
288
+ return MemoryRecord(
289
+ id=memory_id,
290
+ content=content or existing.content,
291
+ created_at=existing.created_at,
292
+ memory_type=existing.memory_type,
293
+ importance=importance if importance is not None else existing.importance,
294
+ project_id=existing.project_id,
295
+ user_id=existing.user_id,
296
+ tags=tags if tags is not None else existing.tags,
297
+ source_type=existing.source_type,
298
+ source_session_id=existing.source_session_id,
299
+ metadata=existing.metadata,
300
+ )
301
+
302
+ async def delete(self, memory_id: str) -> bool:
303
+ """Delete a memory from MemU.
304
+
305
+ Args:
306
+ memory_id: The memory ID to delete
307
+
308
+ Returns:
309
+ True if deleted, False if not found
310
+ """
311
+ try:
312
+ # Run in thread to avoid blocking event loop
313
+ await asyncio.to_thread(self._service.delete_memory_item, memory_id=memory_id)
314
+ return True
315
+ except Exception:
316
+ return False
317
+
318
+ async def search(self, query: MemoryQuery) -> list[MemoryRecord]:
319
+ """Search for memories using MemU's semantic search.
320
+
321
+ Args:
322
+ query: Search parameters
323
+
324
+ Returns:
325
+ List of matching MemoryRecords
326
+ """
327
+ user_id = query.user_id or self._default_user_id or "default"
328
+
329
+ # Build query for MemU retrieve
330
+ queries = [{"role": "user", "content": query.text or ""}]
331
+
332
+ # Build where filter
333
+ where_filter: dict[str, Any] = {"user_id": user_id}
334
+ if query.project_id:
335
+ where_filter["project_id"] = query.project_id
336
+
337
+ # Run in thread to avoid blocking event loop
338
+ results = await asyncio.to_thread(
339
+ self._service.retrieve, queries=queries, where=where_filter
340
+ )
341
+
342
+ records = []
343
+ items = results.get("items", results.get("memories", []))
344
+
345
+ for memu_item in items: # Don't slice here - filter first, then limit
346
+ record = self._memu_to_record(memu_item)
347
+
348
+ # Apply additional filters not supported by MemU API
349
+ if query.min_importance is not None and record.importance < query.min_importance:
350
+ continue
351
+ if query.memory_type is not None and record.memory_type != query.memory_type:
352
+ continue
353
+
354
+ records.append(record)
355
+
356
+ # Apply limit after filtering
357
+ if query.limit and len(records) >= query.limit:
358
+ break
359
+
360
+ return records
361
+
362
+ async def list_memories(
363
+ self,
364
+ project_id: str | None = None,
365
+ user_id: str | None = None,
366
+ memory_type: str | None = None,
367
+ limit: int = 50,
368
+ offset: int = 0,
369
+ ) -> list[MemoryRecord]:
370
+ """List memories from MemU with optional filtering.
371
+
372
+ Args:
373
+ project_id: Filter by project ID
374
+ user_id: Filter by user ID
375
+ memory_type: Filter by memory type
376
+ limit: Maximum number of results
377
+ offset: Number of results to skip
378
+
379
+ Returns:
380
+ List of MemoryRecords
381
+ """
382
+ effective_user_id = user_id or self._default_user_id or "default"
383
+
384
+ # Build where filter
385
+ where_filter: dict[str, Any] = {"user_id": effective_user_id}
386
+ if project_id:
387
+ where_filter["project_id"] = project_id
388
+
389
+ # Run in thread to avoid blocking event loop
390
+ results = await asyncio.to_thread(self._service.list_memory_items, where=where_filter)
391
+
392
+ items = results.get("items", results.get("memories", []))
393
+
394
+ # First filter all items by memory_type
395
+ filtered_items = []
396
+ for memu_item in items:
397
+ record = self._memu_to_record(memu_item)
398
+ if memory_type is not None and record.memory_type != memory_type:
399
+ continue
400
+ filtered_items.append(record)
401
+
402
+ # Then apply pagination
403
+ return filtered_items[offset : offset + limit]
404
+
405
+ def close(self) -> None:
406
+ """Clean up resources.
407
+
408
+ Called when the backend is no longer needed.
409
+ """
410
+ # MemU service doesn't require explicit cleanup
411
+ pass
412
+
413
+ def _map_memory_type(self, memory_type: str) -> str:
414
+ """Map our memory types to MemU MemoryType strings.
415
+
416
+ Args:
417
+ memory_type: Our memory type string
418
+
419
+ Returns:
420
+ MemU MemoryType string (one of: profile, event, knowledge, behavior, skill)
421
+ """
422
+ # MemU uses Literal type: Literal["profile", "event", "knowledge", "behavior", "skill"]
423
+ type_mapping = {
424
+ "fact": "knowledge",
425
+ "knowledge": "knowledge",
426
+ "preference": "profile",
427
+ "profile": "profile",
428
+ "event": "event",
429
+ "skill": "skill",
430
+ "behavior": "behavior",
431
+ }
432
+ return type_mapping.get(memory_type, "knowledge")
433
+
434
+ def _memu_to_record(
435
+ self,
436
+ memu_item: dict[str, Any],
437
+ ) -> MemoryRecord:
438
+ """Convert a MemU memory dict to MemoryRecord.
439
+
440
+ Args:
441
+ memu_item: Memory dict from MemU API
442
+
443
+ Returns:
444
+ MemoryRecord instance
445
+ """
446
+ created_at_str = memu_item.get("created_at")
447
+ if created_at_str:
448
+ if isinstance(created_at_str, str):
449
+ created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
450
+ else:
451
+ created_at = created_at_str
452
+ else:
453
+ created_at = datetime.now(UTC)
454
+
455
+ # Extract memory type from MemU's memory_type field
456
+ memu_type = memu_item.get("memory_type", "knowledge")
457
+ if hasattr(memu_type, "value"):
458
+ memu_type = memu_type.value
459
+
460
+ # Map MemU types back to our types
461
+ type_mapping = {
462
+ "knowledge": "fact",
463
+ "profile": "preference",
464
+ "event": "event",
465
+ "skill": "skill",
466
+ "behavior": "behavior",
467
+ }
468
+ memory_type = type_mapping.get(str(memu_type), "fact")
469
+
470
+ # Get categories as tags
471
+ categories = memu_item.get("categories", memu_item.get("memory_categories", []))
472
+
473
+ return MemoryRecord(
474
+ id=memu_item.get("id") or memu_item.get("memory_id", "unknown"),
475
+ content=memu_item.get("content") or memu_item.get("memory_content", ""),
476
+ created_at=created_at,
477
+ memory_type=memory_type,
478
+ importance=memu_item.get("importance", 0.5),
479
+ project_id=memu_item.get("project_id"),
480
+ user_id=memu_item.get("user_id"),
481
+ tags=categories if isinstance(categories, list) else [],
482
+ source_type=memu_item.get("source_type"),
483
+ source_session_id=memu_item.get("source_session_id"),
484
+ metadata=memu_item.get("metadata", {}),
485
+ )
@@ -0,0 +1,111 @@
1
+ """Null memory backend for testing.
2
+
3
+ This backend provides a no-op implementation that satisfies the protocol
4
+ but doesn't persist any data. Useful for:
5
+ - Unit tests that don't need real storage
6
+ - Integration tests with isolated memory
7
+ - Dry-run scenarios
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import UTC, datetime
13
+ from typing import Any
14
+ from uuid import uuid4
15
+
16
+ from gobby.memory.protocol import (
17
+ MediaAttachment,
18
+ MemoryCapability,
19
+ MemoryQuery,
20
+ MemoryRecord,
21
+ )
22
+
23
+
24
+ class NullBackend:
25
+ """A no-op memory backend for testing.
26
+
27
+ Creates memories in-memory but doesn't persist them.
28
+ Searches always return empty results.
29
+ """
30
+
31
+ def capabilities(self) -> set[MemoryCapability]:
32
+ """Return supported capabilities."""
33
+ return {
34
+ MemoryCapability.CREATE,
35
+ MemoryCapability.READ,
36
+ MemoryCapability.UPDATE,
37
+ MemoryCapability.DELETE,
38
+ MemoryCapability.SEARCH,
39
+ MemoryCapability.LIST,
40
+ }
41
+
42
+ async def create(
43
+ self,
44
+ content: str,
45
+ memory_type: str = "fact",
46
+ importance: float = 0.5,
47
+ project_id: str | None = None,
48
+ user_id: str | None = None,
49
+ tags: list[str] | None = None,
50
+ source_type: str | None = None,
51
+ source_session_id: str | None = None,
52
+ media: list[MediaAttachment] | None = None,
53
+ metadata: dict[str, Any] | None = None,
54
+ ) -> MemoryRecord:
55
+ """Create a memory record (in-memory only, not persisted)."""
56
+ now = datetime.now(UTC)
57
+ return MemoryRecord(
58
+ id=f"null-{uuid4().hex[:8]}",
59
+ content=content,
60
+ created_at=now,
61
+ memory_type=memory_type,
62
+ importance=importance,
63
+ project_id=project_id,
64
+ user_id=user_id,
65
+ tags=tags or [],
66
+ source_type=source_type,
67
+ source_session_id=source_session_id,
68
+ media=media or [],
69
+ metadata=metadata or {},
70
+ )
71
+
72
+ async def get(self, memory_id: str) -> MemoryRecord | None:
73
+ """Get a memory by ID (always returns None - no persistence)."""
74
+ return None
75
+
76
+ async def update(
77
+ self,
78
+ memory_id: str,
79
+ content: str | None = None,
80
+ importance: float | None = None,
81
+ tags: list[str] | None = None,
82
+ ) -> MemoryRecord:
83
+ """Update a memory (creates a new record since nothing is persisted)."""
84
+ now = datetime.now(UTC)
85
+ return MemoryRecord(
86
+ id=memory_id,
87
+ content=content or "",
88
+ created_at=now,
89
+ updated_at=now,
90
+ importance=importance if importance is not None else 0.5,
91
+ tags=tags or [],
92
+ )
93
+
94
+ async def delete(self, memory_id: str) -> bool:
95
+ """Delete a memory (always returns False - nothing to delete)."""
96
+ return False
97
+
98
+ async def search(self, query: MemoryQuery) -> list[MemoryRecord]:
99
+ """Search for memories (always returns empty list)."""
100
+ return []
101
+
102
+ async def list_memories(
103
+ self,
104
+ project_id: str | None = None,
105
+ user_id: str | None = None,
106
+ memory_type: str | None = None,
107
+ limit: int = 50,
108
+ offset: int = 0,
109
+ ) -> list[MemoryRecord]:
110
+ """List memories (always returns empty list)."""
111
+ return []