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,451 @@
1
+ """Memory backend protocol types.
2
+
3
+ This module defines the abstraction layer that enables pluggable memory backends.
4
+ Users can choose between Gobby's built-in SQLite backend or plug in external
5
+ memory systems like Mem0, OpenMemory, or MemU.
6
+
7
+ Types:
8
+ - MemoryCapability: Enum of capabilities a backend can support
9
+ - MemoryQuery: Dataclass for search parameters
10
+ - MediaAttachment: Dataclass for multimodal memory support
11
+ - MemoryRecord: Backend-agnostic memory representation
12
+ - MemoryBackendProtocol: Protocol interface that backends must implement
13
+
14
+ Example:
15
+ from gobby.memory.protocol import MemoryBackendProtocol, MemoryCapability
16
+
17
+ class MyBackend:
18
+ def capabilities(self) -> set[MemoryCapability]:
19
+ return {MemoryCapability.CREATE, MemoryCapability.READ}
20
+ # ... implement other required methods
21
+
22
+ assert isinstance(MyBackend(), MemoryBackendProtocol)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+ from datetime import UTC, datetime
29
+ from enum import Enum
30
+ from typing import Any, Protocol, runtime_checkable
31
+
32
+ __all__ = [
33
+ "MemoryCapability",
34
+ "MemoryQuery",
35
+ "MediaAttachment",
36
+ "MemoryRecord",
37
+ "MemoryBackendProtocol",
38
+ ]
39
+
40
+
41
+ class MemoryCapability(Enum):
42
+ """Capabilities that a memory backend can support.
43
+
44
+ Backends declare which capabilities they support via the capabilities()
45
+ method. The MemoryManager uses these to gracefully degrade when a backend
46
+ doesn't support a requested operation.
47
+
48
+ Basic CRUD:
49
+ CREATE: Store new memories
50
+ READ: Retrieve a specific memory by ID
51
+ UPDATE: Modify existing memories
52
+ DELETE: Remove memories
53
+
54
+ Search capabilities:
55
+ SEARCH_TEXT: Text-based substring/keyword search
56
+ SEARCH_SEMANTIC: Embedding-based semantic similarity search
57
+ SEARCH_HYBRID: Combined text + semantic search
58
+
59
+ Advanced features:
60
+ TAGS: Tag-based filtering and organization
61
+ IMPORTANCE: Importance scoring and filtering
62
+ CROSSREF: Cross-referencing between related memories
63
+ MEDIA: Multimodal memory support (images, etc.)
64
+ DECAY: Time-based importance decay
65
+
66
+ MCP-aligned operations (aliases for compatibility):
67
+ REMEMBER: Alias for CREATE
68
+ RECALL: Alias for READ + SEARCH
69
+ FORGET: Alias for DELETE
70
+ SEARCH: Generic search (text or semantic)
71
+ LIST: List/enumerate memories
72
+ EXISTS: Check if memory exists
73
+ STATS: Get statistics about memories
74
+ """
75
+
76
+ # Basic CRUD
77
+ CREATE = "create"
78
+ READ = "read"
79
+ UPDATE = "update"
80
+ DELETE = "delete"
81
+
82
+ # Search capabilities
83
+ SEARCH_TEXT = "search_text"
84
+ SEARCH_SEMANTIC = "search_semantic"
85
+ SEARCH_HYBRID = "search_hybrid"
86
+
87
+ # Advanced features
88
+ TAGS = "tags"
89
+ IMPORTANCE = "importance"
90
+ CROSSREF = "crossref"
91
+ MEDIA = "media"
92
+ DECAY = "decay"
93
+
94
+ # MCP-aligned operations (aliases)
95
+ REMEMBER = "remember"
96
+ RECALL = "recall"
97
+ FORGET = "forget"
98
+ SEARCH = "search"
99
+ LIST = "list"
100
+ EXISTS = "exists"
101
+ STATS = "stats"
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class MemoryQuery:
106
+ """Search parameters for memory recall operations.
107
+
108
+ Attributes:
109
+ text: Search query text (required for search operations)
110
+ project_id: Filter by project ID
111
+ user_id: Filter by user ID (for multi-tenant backends)
112
+ limit: Maximum number of results to return
113
+ min_importance: Minimum importance threshold
114
+ memory_type: Filter by memory type (fact, preference, etc.)
115
+ tags_all: Memory must have ALL of these tags
116
+ tags_any: Memory must have at least ONE of these tags
117
+ tags_none: Memory must have NONE of these tags
118
+ search_mode: Search mode - "auto", "text", "semantic", "hybrid"
119
+
120
+ Example:
121
+ query = MemoryQuery(
122
+ text="authentication",
123
+ project_id="proj-123",
124
+ min_importance=0.5,
125
+ tags_all=["security"],
126
+ search_mode="semantic"
127
+ )
128
+ """
129
+
130
+ text: str
131
+ project_id: str | None = None
132
+ user_id: str | None = None
133
+ limit: int = 10
134
+ min_importance: float | None = None
135
+ memory_type: str | None = None
136
+ tags_all: list[str] | None = None
137
+ tags_any: list[str] | None = None
138
+ tags_none: list[str] | None = None
139
+ search_mode: str = "auto"
140
+
141
+
142
+ @dataclass
143
+ class MediaAttachment:
144
+ """Media attachment for multimodal memory support.
145
+
146
+ Enables memories to include images, documents, or other media files.
147
+ The description field can be populated by an LLM to make the media
148
+ searchable via text queries.
149
+
150
+ Attributes:
151
+ media_type: Type of media (e.g., "image", "document", "audio")
152
+ content_path: Path to the media file
153
+ mime_type: MIME type of the media (e.g., "image/png")
154
+ description: LLM-generated description of the media content
155
+ description_model: Model used to generate the description
156
+ metadata: Additional media-specific metadata
157
+
158
+ Example:
159
+ attachment = MediaAttachment(
160
+ media_type="image",
161
+ content_path="/path/to/diagram.png",
162
+ mime_type="image/png",
163
+ description="Architecture diagram showing microservices layout",
164
+ description_model="claude-3-haiku"
165
+ )
166
+ """
167
+
168
+ media_type: str
169
+ content_path: str
170
+ mime_type: str
171
+ description: str | None = None
172
+ description_model: str | None = None
173
+ metadata: dict[str, Any] | None = None
174
+
175
+
176
+ @dataclass
177
+ class MemoryRecord:
178
+ """Backend-agnostic representation of a memory.
179
+
180
+ This is the common format used across all backends. Backends convert
181
+ their internal representations to/from this format.
182
+
183
+ Attributes:
184
+ id: Unique identifier for the memory
185
+ content: The memory content text
186
+ created_at: When the memory was created
187
+ memory_type: Type of memory (fact, preference, pattern, context)
188
+ updated_at: When the memory was last updated
189
+ project_id: Associated project ID
190
+ user_id: Associated user ID (for multi-tenant backends)
191
+ importance: Importance score (0.0 to 1.0)
192
+ tags: List of tags for organization
193
+ source_type: Origin of memory (user, session, inferred)
194
+ source_session_id: Session that created the memory
195
+ access_count: Number of times memory was accessed
196
+ last_accessed_at: When memory was last accessed
197
+ media: List of media attachments
198
+ metadata: Additional backend-specific metadata
199
+
200
+ Example:
201
+ record = MemoryRecord(
202
+ id="mem-abc123",
203
+ content="User prefers dark mode",
204
+ created_at=datetime.now(UTC),
205
+ memory_type="preference",
206
+ importance=0.8,
207
+ tags=["ui", "settings"]
208
+ )
209
+ """
210
+
211
+ id: str
212
+ content: str
213
+ created_at: datetime
214
+ memory_type: str = "fact"
215
+ updated_at: datetime | None = None
216
+ project_id: str | None = None
217
+ user_id: str | None = None
218
+ importance: float = 0.5
219
+ tags: list[str] = field(default_factory=list)
220
+ source_type: str | None = None
221
+ source_session_id: str | None = None
222
+ access_count: int = 0
223
+ last_accessed_at: datetime | None = None
224
+ media: list[MediaAttachment] = field(default_factory=list)
225
+ metadata: dict[str, Any] = field(default_factory=dict)
226
+
227
+ def to_dict(self) -> dict[str, Any]:
228
+ """Convert record to dictionary for serialization."""
229
+ return {
230
+ "id": self.id,
231
+ "content": self.content,
232
+ "created_at": self.created_at.isoformat() if self.created_at else None,
233
+ "memory_type": self.memory_type,
234
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None,
235
+ "project_id": self.project_id,
236
+ "user_id": self.user_id,
237
+ "importance": self.importance,
238
+ "tags": self.tags,
239
+ "source_type": self.source_type,
240
+ "source_session_id": self.source_session_id,
241
+ "access_count": self.access_count,
242
+ "last_accessed_at": (
243
+ self.last_accessed_at.isoformat() if self.last_accessed_at else None
244
+ ),
245
+ "media": [
246
+ {
247
+ "media_type": m.media_type,
248
+ "content_path": m.content_path,
249
+ "mime_type": m.mime_type,
250
+ "description": m.description,
251
+ "description_model": m.description_model,
252
+ "metadata": m.metadata,
253
+ }
254
+ for m in (self.media or [])
255
+ ],
256
+ "metadata": self.metadata,
257
+ }
258
+
259
+ @classmethod
260
+ def from_dict(cls, data: dict[str, Any]) -> MemoryRecord:
261
+ """Create record from dictionary."""
262
+ # Parse datetime fields
263
+ created_at = data.get("created_at")
264
+ if isinstance(created_at, str):
265
+ created_at = datetime.fromisoformat(created_at)
266
+ elif created_at is None:
267
+ created_at = datetime.now(UTC)
268
+
269
+ updated_at = data.get("updated_at")
270
+ if isinstance(updated_at, str):
271
+ updated_at = datetime.fromisoformat(updated_at)
272
+
273
+ last_accessed_at = data.get("last_accessed_at")
274
+ if isinstance(last_accessed_at, str):
275
+ last_accessed_at = datetime.fromisoformat(last_accessed_at)
276
+
277
+ # Parse media attachments
278
+ media_data = data.get("media", [])
279
+ media = [
280
+ MediaAttachment(
281
+ media_type=m.get("media_type", "unknown"),
282
+ content_path=m.get("content_path", ""),
283
+ mime_type=m.get("mime_type", "application/octet-stream"),
284
+ description=m.get("description"),
285
+ description_model=m.get("description_model"),
286
+ metadata=m.get("metadata"),
287
+ )
288
+ for m in media_data
289
+ ]
290
+
291
+ return cls(
292
+ id=data["id"],
293
+ content=data["content"],
294
+ created_at=created_at,
295
+ memory_type=data.get("memory_type", "fact"),
296
+ updated_at=updated_at,
297
+ project_id=data.get("project_id"),
298
+ user_id=data.get("user_id"),
299
+ importance=data.get("importance", 0.5),
300
+ tags=data.get("tags", []),
301
+ source_type=data.get("source_type"),
302
+ source_session_id=data.get("source_session_id"),
303
+ access_count=data.get("access_count", 0),
304
+ last_accessed_at=last_accessed_at,
305
+ media=media,
306
+ metadata=data.get("metadata", {}),
307
+ )
308
+
309
+
310
+ @runtime_checkable
311
+ class MemoryBackendProtocol(Protocol):
312
+ """Protocol interface that memory backends must implement.
313
+
314
+ Backends can implement a subset of methods based on their capabilities.
315
+ The capabilities() method declares which operations the backend supports,
316
+ allowing the MemoryManager to gracefully degrade for unsupported operations.
317
+
318
+ Required methods:
319
+ capabilities(): Return set of supported MemoryCapability values
320
+ create(): Store a new memory
321
+ get(): Retrieve a memory by ID
322
+ update(): Update an existing memory
323
+ delete(): Delete a memory
324
+ search(): Search for memories
325
+ list_memories(): List memories with filtering
326
+
327
+ Example:
328
+ class MyBackend:
329
+ def capabilities(self) -> set[MemoryCapability]:
330
+ return {MemoryCapability.CREATE, MemoryCapability.READ}
331
+
332
+ async def create(self, content: str, **kwargs) -> MemoryRecord:
333
+ # Implementation...
334
+
335
+ backend = MyBackend()
336
+ assert isinstance(backend, MemoryBackendProtocol)
337
+ """
338
+
339
+ def capabilities(self) -> set[MemoryCapability]:
340
+ """Return the set of capabilities this backend supports."""
341
+ ...
342
+
343
+ async def create(
344
+ self,
345
+ content: str,
346
+ memory_type: str = "fact",
347
+ importance: float = 0.5,
348
+ project_id: str | None = None,
349
+ user_id: str | None = None,
350
+ tags: list[str] | None = None,
351
+ source_type: str | None = None,
352
+ source_session_id: str | None = None,
353
+ media: list[MediaAttachment] | None = None,
354
+ metadata: dict[str, Any] | None = None,
355
+ ) -> MemoryRecord:
356
+ """Create a new memory.
357
+
358
+ Args:
359
+ content: The memory content text
360
+ memory_type: Type of memory (fact, preference, etc.)
361
+ importance: Importance score (0.0 to 1.0)
362
+ project_id: Associated project ID
363
+ user_id: Associated user ID
364
+ tags: List of tags
365
+ source_type: Origin of memory
366
+ source_session_id: Session that created the memory
367
+ media: List of media attachments
368
+ metadata: Additional metadata
369
+
370
+ Returns:
371
+ The created MemoryRecord
372
+ """
373
+ ...
374
+
375
+ async def get(self, memory_id: str) -> MemoryRecord | None:
376
+ """Retrieve a memory by ID.
377
+
378
+ Args:
379
+ memory_id: The memory ID to retrieve
380
+
381
+ Returns:
382
+ The MemoryRecord if found, None otherwise
383
+ """
384
+ ...
385
+
386
+ async def update(
387
+ self,
388
+ memory_id: str,
389
+ content: str | None = None,
390
+ importance: float | None = None,
391
+ tags: list[str] | None = None,
392
+ ) -> MemoryRecord:
393
+ """Update an existing memory.
394
+
395
+ Args:
396
+ memory_id: The memory ID to update
397
+ content: New content (optional)
398
+ importance: New importance score (optional)
399
+ tags: New tags (optional)
400
+
401
+ Returns:
402
+ The updated MemoryRecord
403
+
404
+ Raises:
405
+ ValueError: If memory not found
406
+ """
407
+ ...
408
+
409
+ async def delete(self, memory_id: str) -> bool:
410
+ """Delete a memory.
411
+
412
+ Args:
413
+ memory_id: The memory ID to delete
414
+
415
+ Returns:
416
+ True if deleted, False if not found
417
+ """
418
+ ...
419
+
420
+ async def search(self, query: MemoryQuery) -> list[MemoryRecord]:
421
+ """Search for memories.
422
+
423
+ Args:
424
+ query: Search parameters
425
+
426
+ Returns:
427
+ List of matching MemoryRecords
428
+ """
429
+ ...
430
+
431
+ async def list_memories(
432
+ self,
433
+ project_id: str | None = None,
434
+ user_id: str | None = None,
435
+ memory_type: str | None = None,
436
+ limit: int = 50,
437
+ offset: int = 0,
438
+ ) -> list[MemoryRecord]:
439
+ """List memories with optional filtering.
440
+
441
+ Args:
442
+ project_id: Filter by project ID
443
+ user_id: Filter by user ID
444
+ memory_type: Filter by memory type
445
+ limit: Maximum number of results
446
+ offset: Number of results to skip
447
+
448
+ Returns:
449
+ List of MemoryRecords
450
+ """
451
+ ...
@@ -0,0 +1,66 @@
1
+ """
2
+ Memory search backend abstraction.
3
+
4
+ Provides pluggable search backends for memory recall:
5
+ - TF-IDF (default) - Zero-dependency local search using sklearn
6
+ - Text - Simple substring matching fallback
7
+
8
+ This module re-exports the shared search components from gobby.search
9
+ and adds memory-specific TextSearcher backend.
10
+
11
+ Usage:
12
+ from gobby.memory.search import SearchBackend, get_search_backend
13
+
14
+ backend = get_search_backend("tfidf")
15
+ backend.fit([(id, content) for id, content in memories])
16
+ results = backend.search("query text", top_k=10)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any, cast
22
+
23
+ # Re-export shared search components for backwards compatibility
24
+ from gobby.search import SearchBackend, SearchResult, TFIDFSearcher
25
+
26
+ if TYPE_CHECKING:
27
+ from gobby.storage.database import DatabaseProtocol
28
+
29
+ __all__ = [
30
+ "SearchBackend",
31
+ "SearchResult",
32
+ "TFIDFSearcher",
33
+ "get_search_backend",
34
+ ]
35
+
36
+
37
+ def get_search_backend(
38
+ backend_type: str,
39
+ db: DatabaseProtocol | None = None,
40
+ **kwargs: Any,
41
+ ) -> SearchBackend:
42
+ """
43
+ Factory function for search backends.
44
+
45
+ Args:
46
+ backend_type: Type of backend - "tfidf" or "text"
47
+ db: Database connection (unused, kept for backwards compatibility)
48
+ **kwargs: Backend-specific configuration
49
+
50
+ Returns:
51
+ SearchBackend instance
52
+
53
+ Raises:
54
+ ValueError: If backend_type is unknown
55
+ ImportError: If required dependencies are not installed
56
+ """
57
+ if backend_type == "tfidf":
58
+ return cast(SearchBackend, TFIDFSearcher(**kwargs))
59
+
60
+ elif backend_type == "text":
61
+ from gobby.memory.search.text import TextSearcher
62
+
63
+ return cast(SearchBackend, TextSearcher(**kwargs))
64
+
65
+ else:
66
+ raise ValueError(f"Unknown search backend: {backend_type}. Valid options: tfidf, text")
@@ -0,0 +1,127 @@
1
+ """
2
+ Simple text-based search backend (fallback).
3
+
4
+ Provides basic substring matching when no other backend is available.
5
+ This is the fallback when sklearn is not installed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class TextSearcher:
12
+ """
13
+ Simple text-based search using substring matching.
14
+
15
+ This is a fallback backend that works without any dependencies.
16
+ It provides basic relevance scoring based on:
17
+ - Exact phrase match (highest score)
18
+ - Word overlap (proportional score)
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ self._memories: dict[str, str] = {} # id -> content
23
+ self._fitted = False
24
+
25
+ def fit(self, memories: list[tuple[str, str]]) -> None:
26
+ """
27
+ Build index from memories.
28
+
29
+ Args:
30
+ memories: List of (memory_id, content) tuples
31
+ """
32
+ self._memories = {mid: content.lower() for mid, content in memories}
33
+ self._fitted = True
34
+
35
+ def search(self, query: str, top_k: int = 10) -> list[tuple[str, float]]:
36
+ """
37
+ Search for memories containing query terms.
38
+
39
+ Args:
40
+ query: Search query text
41
+ top_k: Maximum results to return
42
+
43
+ Returns:
44
+ List of (memory_id, similarity_score) tuples
45
+ """
46
+ if not self._fitted or not self._memories:
47
+ return []
48
+
49
+ query_lower = query.lower()
50
+ query_words = set(query_lower.split())
51
+ results: list[tuple[str, float]] = []
52
+
53
+ for memory_id, content in self._memories.items():
54
+ score = self._score_match(query_lower, query_words, content)
55
+ if score > 0:
56
+ results.append((memory_id, score))
57
+
58
+ # Sort by score descending
59
+ results.sort(key=lambda x: x[1], reverse=True)
60
+ return results[:top_k]
61
+
62
+ def _score_match(
63
+ self,
64
+ query: str,
65
+ query_words: set[str],
66
+ content: str,
67
+ ) -> float:
68
+ """
69
+ Score a content match against query.
70
+
71
+ Scoring:
72
+ - Exact phrase match: 1.0
73
+ - All words present: 0.8
74
+ - Partial word match: proportion of words found * 0.6
75
+ """
76
+ # Exact phrase match
77
+ if query in content:
78
+ return 1.0
79
+
80
+ # Word-based matching
81
+ content_words = set(content.split())
82
+ matching_words = query_words & content_words
83
+
84
+ if not matching_words:
85
+ return 0.0
86
+
87
+ # All words present
88
+ if matching_words == query_words:
89
+ return 0.8
90
+
91
+ # Partial match - proportion of query words found
92
+ match_ratio = len(matching_words) / len(query_words)
93
+ return match_ratio * 0.6
94
+
95
+ def needs_refit(self) -> bool:
96
+ """Check if index needs rebuilding."""
97
+ return not self._fitted
98
+
99
+ def add_memory(self, memory_id: str, content: str) -> None:
100
+ """
101
+ Add a single memory to the index (incremental update).
102
+
103
+ Args:
104
+ memory_id: Memory ID
105
+ content: Memory content
106
+ """
107
+ self._memories[memory_id] = content.lower()
108
+
109
+ def remove_memory(self, memory_id: str) -> bool:
110
+ """
111
+ Remove a memory from the index.
112
+
113
+ Args:
114
+ memory_id: Memory ID to remove
115
+
116
+ Returns:
117
+ True if memory was removed, False if not found
118
+ """
119
+ if memory_id in self._memories:
120
+ del self._memories[memory_id]
121
+ return True
122
+ return False
123
+
124
+ def clear(self) -> None:
125
+ """Clear the search index."""
126
+ self._memories.clear()
127
+ self._fitted = False