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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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
|
+
)
|