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,537 @@
1
+ """OpenMemory REST API backend integration.
2
+
3
+ This backend connects to a self-hosted OpenMemory server to provide
4
+ embedding-based semantic memory storage and search.
5
+
6
+ OpenMemory is a self-hosted memory system that provides:
7
+ - REST API for CRUD operations on memories
8
+ - Embedding-based semantic search
9
+ - Local storage (no cloud dependency)
10
+
11
+ Example:
12
+ from gobby.memory.backends import get_backend
13
+
14
+ backend = get_backend("openmemory", base_url="http://localhost:8080")
15
+ record = await backend.create("User prefers dark mode")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from datetime import UTC, datetime
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import httpx
25
+
26
+ from gobby.memory.protocol import (
27
+ MediaAttachment,
28
+ MemoryCapability,
29
+ MemoryQuery,
30
+ MemoryRecord,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ pass
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class OpenMemoryError(Exception):
40
+ """Base exception for OpenMemory backend errors."""
41
+
42
+ pass
43
+
44
+
45
+ class OpenMemoryConnectionError(OpenMemoryError):
46
+ """Raised when connection to OpenMemory server fails."""
47
+
48
+ pass
49
+
50
+
51
+ class OpenMemoryAPIError(OpenMemoryError):
52
+ """Raised when OpenMemory API returns an error."""
53
+
54
+ def __init__(self, message: str, status_code: int | None = None):
55
+ super().__init__(message)
56
+ self.status_code = status_code
57
+
58
+
59
+ class OpenMemoryBackend:
60
+ """OpenMemory REST API backend.
61
+
62
+ Connects to a self-hosted OpenMemory server for embedding-based
63
+ memory storage and semantic search.
64
+
65
+ Args:
66
+ base_url: OpenMemory server base URL (e.g., "http://localhost:8080")
67
+ api_key: Optional API key for authentication
68
+ user_id: Default user ID for memories
69
+ timeout: HTTP request timeout in seconds
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ base_url: str,
75
+ api_key: str | None = None,
76
+ user_id: str | None = None,
77
+ timeout: float = 30.0,
78
+ ):
79
+ """Initialize the OpenMemory backend.
80
+
81
+ Args:
82
+ base_url: Server base URL (no trailing slash)
83
+ api_key: Optional API key for authentication
84
+ user_id: Default user ID for operations
85
+ timeout: Request timeout in seconds
86
+ """
87
+ # Normalize base URL
88
+ self._base_url = base_url.rstrip("/")
89
+ self._api_key = api_key
90
+ self._default_user_id = user_id or "default"
91
+ self._timeout = timeout
92
+ self._client: httpx.AsyncClient | None = None
93
+
94
+ async def _get_client(self) -> httpx.AsyncClient:
95
+ """Get or create the HTTP client."""
96
+ if self._client is None or self._client.is_closed:
97
+ headers = {"Content-Type": "application/json"}
98
+ if self._api_key:
99
+ headers["Authorization"] = f"Bearer {self._api_key}"
100
+ self._client = httpx.AsyncClient(
101
+ base_url=self._base_url,
102
+ headers=headers,
103
+ timeout=self._timeout,
104
+ )
105
+ return self._client
106
+
107
+ async def close(self) -> None:
108
+ """Close the HTTP client."""
109
+ if self._client and not self._client.is_closed:
110
+ await self._client.aclose()
111
+ self._client = None
112
+
113
+ async def __aenter__(self) -> OpenMemoryBackend:
114
+ """Async context manager entry."""
115
+ return self
116
+
117
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
118
+ """Async context manager exit."""
119
+ await self.close()
120
+
121
+ def capabilities(self) -> set[MemoryCapability]:
122
+ """Return supported capabilities.
123
+
124
+ OpenMemory supports semantic search and basic CRUD operations.
125
+ """
126
+ return {
127
+ # Basic CRUD
128
+ MemoryCapability.CREATE,
129
+ MemoryCapability.READ,
130
+ MemoryCapability.UPDATE,
131
+ MemoryCapability.DELETE,
132
+ # Search
133
+ MemoryCapability.SEARCH_SEMANTIC,
134
+ MemoryCapability.SEARCH,
135
+ # Advanced
136
+ MemoryCapability.LIST,
137
+ MemoryCapability.TAGS,
138
+ MemoryCapability.IMPORTANCE,
139
+ # MCP-aligned
140
+ MemoryCapability.REMEMBER,
141
+ MemoryCapability.RECALL,
142
+ MemoryCapability.FORGET,
143
+ }
144
+
145
+ async def create(
146
+ self,
147
+ content: str,
148
+ memory_type: str = "fact",
149
+ importance: float = 0.5,
150
+ project_id: str | None = None,
151
+ user_id: str | None = None,
152
+ tags: list[str] | None = None,
153
+ source_type: str | None = None,
154
+ source_session_id: str | None = None,
155
+ media: list[MediaAttachment] | None = None,
156
+ metadata: dict[str, Any] | None = None,
157
+ ) -> MemoryRecord:
158
+ """Create a new memory in OpenMemory.
159
+
160
+ Args:
161
+ content: The memory content text
162
+ memory_type: Type of memory (stored in metadata)
163
+ importance: Importance score (stored in metadata)
164
+ project_id: Associated project ID
165
+ user_id: User ID (uses default if not provided)
166
+ tags: List of tags
167
+ source_type: Origin of memory
168
+ source_session_id: Session that created the memory
169
+ media: List of media attachments (stored in metadata)
170
+ metadata: Additional metadata
171
+
172
+ Returns:
173
+ The created MemoryRecord
174
+ """
175
+ client = await self._get_client()
176
+ effective_user_id = user_id or self._default_user_id
177
+
178
+ # Build request payload
179
+ payload: dict[str, Any] = {
180
+ "content": content,
181
+ "user_id": effective_user_id,
182
+ "metadata": {
183
+ "memory_type": memory_type,
184
+ "importance": importance,
185
+ "source_type": source_type,
186
+ "source_session_id": source_session_id,
187
+ **(metadata or {}),
188
+ },
189
+ }
190
+ if project_id:
191
+ payload["metadata"]["project_id"] = project_id
192
+ if tags:
193
+ payload["tags"] = tags
194
+ if media:
195
+ payload["metadata"]["media"] = [
196
+ {
197
+ "media_type": m.media_type,
198
+ "content_path": m.content_path,
199
+ "mime_type": m.mime_type,
200
+ "description": m.description,
201
+ }
202
+ for m in media
203
+ ]
204
+
205
+ try:
206
+ response = await client.post("/api/v1/memories", json=payload)
207
+ response.raise_for_status()
208
+ data = response.json()
209
+ return self._response_to_record(data)
210
+ except httpx.ConnectError as e:
211
+ raise OpenMemoryConnectionError(
212
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
213
+ ) from e
214
+ except httpx.HTTPStatusError as e:
215
+ raise OpenMemoryAPIError(
216
+ f"OpenMemory API error: {e.response.text}",
217
+ status_code=e.response.status_code,
218
+ ) from e
219
+ except Exception as e:
220
+ # Log and re-raise - callers should handle failures explicitly
221
+ logger.error(
222
+ f"OpenMemory create failed: {e}",
223
+ exc_info=True,
224
+ )
225
+ raise
226
+
227
+ async def get(self, memory_id: str) -> MemoryRecord | None:
228
+ """Retrieve a memory by ID from OpenMemory.
229
+
230
+ Args:
231
+ memory_id: The memory ID to retrieve
232
+
233
+ Returns:
234
+ The MemoryRecord if found, None otherwise
235
+ """
236
+ client = await self._get_client()
237
+ try:
238
+ response = await client.get(f"/api/v1/memories/{memory_id}")
239
+ if response.status_code == 404:
240
+ return None
241
+ response.raise_for_status()
242
+ data = response.json()
243
+ return self._response_to_record(data)
244
+ except httpx.ConnectError as e:
245
+ raise OpenMemoryConnectionError(
246
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
247
+ ) from e
248
+ except httpx.HTTPStatusError as e:
249
+ if e.response.status_code == 404:
250
+ return None
251
+ raise OpenMemoryAPIError(
252
+ f"OpenMemory API error: {e.response.text}",
253
+ status_code=e.response.status_code,
254
+ ) from e
255
+ except Exception as e:
256
+ logger.error(f"OpenMemory get failed for {memory_id}: {e}", exc_info=True)
257
+ return None
258
+
259
+ async def update(
260
+ self,
261
+ memory_id: str,
262
+ content: str | None = None,
263
+ importance: float | None = None,
264
+ tags: list[str] | None = None,
265
+ ) -> MemoryRecord:
266
+ """Update an existing memory in OpenMemory.
267
+
268
+ Args:
269
+ memory_id: The memory ID to update
270
+ content: New content (optional)
271
+ importance: New importance score (optional)
272
+ tags: New tags (optional)
273
+
274
+ Returns:
275
+ The updated MemoryRecord
276
+
277
+ Raises:
278
+ ValueError: If memory not found
279
+ """
280
+ client = await self._get_client()
281
+
282
+ # Build update payload
283
+ payload: dict[str, Any] = {}
284
+ if content is not None:
285
+ payload["content"] = content
286
+ if importance is not None:
287
+ payload["metadata"] = {"importance": importance}
288
+ if tags is not None:
289
+ payload["tags"] = tags
290
+
291
+ try:
292
+ response = await client.patch(f"/api/v1/memories/{memory_id}", json=payload)
293
+ if response.status_code == 404:
294
+ raise ValueError(f"Memory not found: {memory_id}")
295
+ response.raise_for_status()
296
+ data = response.json()
297
+ return self._response_to_record(data)
298
+ except httpx.ConnectError as e:
299
+ raise OpenMemoryConnectionError(
300
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
301
+ ) from e
302
+ except httpx.HTTPStatusError as e:
303
+ if e.response.status_code == 404:
304
+ raise ValueError(f"Memory not found: {memory_id}") from e
305
+ raise OpenMemoryAPIError(
306
+ f"OpenMemory API error: {e.response.text}",
307
+ status_code=e.response.status_code,
308
+ ) from e
309
+
310
+ async def delete(self, memory_id: str) -> bool:
311
+ """Delete a memory from OpenMemory.
312
+
313
+ Args:
314
+ memory_id: The memory ID to delete
315
+
316
+ Returns:
317
+ True if deleted, False if not found
318
+ """
319
+ client = await self._get_client()
320
+ try:
321
+ response = await client.delete(f"/api/v1/memories/{memory_id}")
322
+ if response.status_code == 404:
323
+ return False
324
+ response.raise_for_status()
325
+ return True
326
+ except httpx.ConnectError as e:
327
+ raise OpenMemoryConnectionError(
328
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
329
+ ) from e
330
+ except httpx.HTTPStatusError as e:
331
+ if e.response.status_code == 404:
332
+ return False
333
+ raise OpenMemoryAPIError(
334
+ f"OpenMemory API error: {e.response.text}",
335
+ status_code=e.response.status_code,
336
+ ) from e
337
+ except Exception as e:
338
+ logger.error(f"OpenMemory delete failed for {memory_id}: {e}", exc_info=True)
339
+ return False
340
+
341
+ async def search(self, query: MemoryQuery) -> list[MemoryRecord]:
342
+ """Search for memories using OpenMemory's semantic search.
343
+
344
+ Args:
345
+ query: Search parameters
346
+
347
+ Returns:
348
+ List of matching MemoryRecords
349
+ """
350
+ client = await self._get_client()
351
+ user_id = query.user_id or self._default_user_id
352
+
353
+ # Build search params
354
+ params: dict[str, Any] = {
355
+ "q": query.text,
356
+ "user_id": user_id,
357
+ "limit": query.limit,
358
+ }
359
+ if query.project_id:
360
+ params["project_id"] = query.project_id
361
+ if query.min_importance is not None:
362
+ params["min_importance"] = query.min_importance
363
+ if query.memory_type:
364
+ params["memory_type"] = query.memory_type
365
+ if query.tags_any:
366
+ params["tags"] = ",".join(query.tags_any)
367
+
368
+ try:
369
+ response = await client.get("/api/v1/memories/search", params=params)
370
+ response.raise_for_status()
371
+ data = response.json()
372
+
373
+ records = []
374
+ for item in data.get("results", data.get("memories", [])):
375
+ record = self._response_to_record(item)
376
+
377
+ # Apply additional filters not supported by API
378
+ if query.tags_all:
379
+ if not all(t in record.tags for t in query.tags_all):
380
+ continue
381
+ if query.tags_none:
382
+ if any(t in record.tags for t in query.tags_none):
383
+ continue
384
+
385
+ records.append(record)
386
+
387
+ return records
388
+ except httpx.ConnectError as e:
389
+ raise OpenMemoryConnectionError(
390
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
391
+ ) from e
392
+ except httpx.HTTPStatusError as e:
393
+ raise OpenMemoryAPIError(
394
+ f"OpenMemory API error: {e.response.text}",
395
+ status_code=e.response.status_code,
396
+ ) from e
397
+ except Exception as e:
398
+ logger.error(f"OpenMemory search failed for query '{query.text}': {e}", exc_info=True)
399
+ return []
400
+
401
+ async def list_memories(
402
+ self,
403
+ project_id: str | None = None,
404
+ user_id: str | None = None,
405
+ memory_type: str | None = None,
406
+ limit: int = 50,
407
+ offset: int = 0,
408
+ ) -> list[MemoryRecord]:
409
+ """List memories from OpenMemory with optional filtering.
410
+
411
+ Args:
412
+ project_id: Filter by project ID
413
+ user_id: Filter by user ID
414
+ memory_type: Filter by memory type
415
+ limit: Maximum number of results
416
+ offset: Number of results to skip
417
+
418
+ Returns:
419
+ List of MemoryRecords
420
+ """
421
+ client = await self._get_client()
422
+ effective_user_id = user_id or self._default_user_id
423
+
424
+ # Build params
425
+ params: dict[str, Any] = {
426
+ "user_id": effective_user_id,
427
+ "limit": limit,
428
+ "offset": offset,
429
+ }
430
+ if project_id:
431
+ params["project_id"] = project_id
432
+ if memory_type:
433
+ params["memory_type"] = memory_type
434
+
435
+ try:
436
+ response = await client.get("/api/v1/memories", params=params)
437
+ response.raise_for_status()
438
+ data = response.json()
439
+
440
+ records = []
441
+ for item in data.get("results", data.get("memories", [])):
442
+ records.append(self._response_to_record(item))
443
+
444
+ return records
445
+ except httpx.ConnectError as e:
446
+ raise OpenMemoryConnectionError(
447
+ f"Failed to connect to OpenMemory at {self._base_url}: {e}"
448
+ ) from e
449
+ except httpx.HTTPStatusError as e:
450
+ raise OpenMemoryAPIError(
451
+ f"OpenMemory API error: {e.response.text}",
452
+ status_code=e.response.status_code,
453
+ ) from e
454
+ except Exception as e:
455
+ logger.error(
456
+ f"OpenMemory list_memories failed (project={project_id}, user={user_id}): {e}",
457
+ exc_info=True,
458
+ )
459
+ return []
460
+
461
+ async def health_check(self) -> bool:
462
+ """Check if the OpenMemory server is healthy.
463
+
464
+ Returns:
465
+ True if server is reachable and healthy, False otherwise
466
+ """
467
+ client = await self._get_client()
468
+ try:
469
+ response = await client.get("/health")
470
+ return response.status_code == 200
471
+ except Exception:
472
+ return False
473
+
474
+ def _response_to_record(self, data: dict[str, Any]) -> MemoryRecord:
475
+ """Convert OpenMemory API response to MemoryRecord.
476
+
477
+ Args:
478
+ data: Response dict from OpenMemory API
479
+
480
+ Returns:
481
+ MemoryRecord instance
482
+
483
+ Raises:
484
+ ValueError: If response is missing required 'id' field
485
+ """
486
+ # Validate that id exists - don't generate synthetic IDs
487
+ if "id" not in data:
488
+ raise ValueError("OpenMemory API response missing required 'id' field")
489
+
490
+ # Parse created_at
491
+ created_at_str = data.get("created_at")
492
+ if created_at_str:
493
+ if isinstance(created_at_str, str):
494
+ created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
495
+ else:
496
+ created_at = datetime.now(UTC)
497
+ else:
498
+ created_at = datetime.now(UTC)
499
+
500
+ # Parse updated_at
501
+ updated_at_str = data.get("updated_at")
502
+ updated_at = None
503
+ if updated_at_str and isinstance(updated_at_str, str):
504
+ updated_at = datetime.fromisoformat(updated_at_str.replace("Z", "+00:00"))
505
+
506
+ # Extract metadata
507
+ metadata = data.get("metadata", {})
508
+
509
+ # Restore media attachments from metadata
510
+ media_list: list[MediaAttachment] = []
511
+ raw_media = metadata.get("media", [])
512
+ for m in raw_media:
513
+ if isinstance(m, dict):
514
+ media_list.append(
515
+ MediaAttachment(
516
+ media_type=m.get("media_type", ""),
517
+ content_path=m.get("content_path", ""),
518
+ mime_type=m.get("mime_type", ""),
519
+ description=m.get("description"),
520
+ )
521
+ )
522
+
523
+ return MemoryRecord(
524
+ id=data["id"],
525
+ content=data.get("content", ""),
526
+ created_at=created_at,
527
+ updated_at=updated_at,
528
+ memory_type=metadata.get("memory_type", "fact"),
529
+ importance=metadata.get("importance", 0.5),
530
+ project_id=metadata.get("project_id"),
531
+ user_id=data.get("user_id"),
532
+ tags=data.get("tags", []),
533
+ source_type=metadata.get("source_type"),
534
+ source_session_id=metadata.get("source_session_id"),
535
+ media=media_list,
536
+ metadata=metadata,
537
+ )