hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperClaw Memory Manager
|
|
3
|
+
Handles memory persistence across sessions with database + file fallback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from hyperclaw import taint
|
|
17
|
+
from hyperclaw.taint import training_sink
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("hyperclaw.memory")
|
|
20
|
+
|
|
21
|
+
# Paths
|
|
22
|
+
HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
|
|
23
|
+
MEMORY_PATH = HYPERCLAW_ROOT / "memory"
|
|
24
|
+
WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Memory:
|
|
29
|
+
"""A single memory entry."""
|
|
30
|
+
id: str
|
|
31
|
+
content: str
|
|
32
|
+
memory_type: str # 'episode', 'semantic', 'instinct', 'dream'
|
|
33
|
+
domain: Optional[str] = None
|
|
34
|
+
importance: float = 0.5
|
|
35
|
+
summary: Optional[str] = None
|
|
36
|
+
source: str = "conversation"
|
|
37
|
+
is_core: bool = False
|
|
38
|
+
metadata: dict = field(default_factory=dict)
|
|
39
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
40
|
+
embedding: Optional[list] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MemoryManager:
|
|
44
|
+
"""
|
|
45
|
+
Manages memory persistence across sessions.
|
|
46
|
+
Uses database when available, falls back to file storage.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, db_pool=None):
|
|
50
|
+
self.db_pool = db_pool
|
|
51
|
+
self._file_cache: dict[str, list[Memory]] = {}
|
|
52
|
+
self._conversation_history: dict[str, list[dict]] = {}
|
|
53
|
+
self._embeddings_client = None
|
|
54
|
+
|
|
55
|
+
async def initialize(self):
|
|
56
|
+
"""Initialize the memory manager."""
|
|
57
|
+
# Ensure directories exist
|
|
58
|
+
MEMORY_PATH.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
(MEMORY_PATH / "daily").mkdir(exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Load file-based memories
|
|
62
|
+
await self._load_file_memories()
|
|
63
|
+
|
|
64
|
+
logger.info("Memory manager initialized")
|
|
65
|
+
|
|
66
|
+
async def _load_file_memories(self):
|
|
67
|
+
"""Load memories from markdown files."""
|
|
68
|
+
# Load instincts
|
|
69
|
+
instincts_file = MEMORY_PATH / "instincts.md"
|
|
70
|
+
if instincts_file.exists():
|
|
71
|
+
content = instincts_file.read_text(encoding="utf-8")
|
|
72
|
+
self._file_cache["instincts"] = self._parse_markdown_list(content, "instinct")
|
|
73
|
+
|
|
74
|
+
# Load core episodes
|
|
75
|
+
episodes_file = MEMORY_PATH / "core-episodes.md"
|
|
76
|
+
if episodes_file.exists():
|
|
77
|
+
content = episodes_file.read_text(encoding="utf-8")
|
|
78
|
+
self._file_cache["episodes"] = self._parse_markdown_list(content, "episode")
|
|
79
|
+
|
|
80
|
+
# Load working memory
|
|
81
|
+
memory_file = WORKSPACE_PATH / "MEMORY.md"
|
|
82
|
+
if memory_file.exists():
|
|
83
|
+
content = memory_file.read_text(encoding="utf-8")
|
|
84
|
+
self._file_cache["working"] = self._parse_markdown_list(content, "semantic")
|
|
85
|
+
|
|
86
|
+
def _parse_markdown_list(self, content: str, memory_type: str) -> list[Memory]:
|
|
87
|
+
"""Parse markdown bullet points into Memory objects."""
|
|
88
|
+
memories = []
|
|
89
|
+
lines = content.split("\n")
|
|
90
|
+
|
|
91
|
+
for i, line in enumerate(lines):
|
|
92
|
+
line = line.strip()
|
|
93
|
+
if line.startswith("- ") or line.startswith("* "):
|
|
94
|
+
text = line[2:].strip()
|
|
95
|
+
if text and not text.startswith("*"): # Skip formatting markers
|
|
96
|
+
memories.append(Memory(
|
|
97
|
+
id=f"file_{memory_type}_{i}",
|
|
98
|
+
content=text,
|
|
99
|
+
memory_type=memory_type,
|
|
100
|
+
source="file",
|
|
101
|
+
is_core=(memory_type in ["instinct", "episode"]),
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
return memories
|
|
105
|
+
|
|
106
|
+
# =========================================================================
|
|
107
|
+
# CONVERSATION HISTORY
|
|
108
|
+
# =========================================================================
|
|
109
|
+
|
|
110
|
+
def get_conversation_history(self, session_id: str, limit: int = 50) -> list[dict]:
|
|
111
|
+
"""Get conversation history for a session."""
|
|
112
|
+
history = self._conversation_history.get(session_id, [])
|
|
113
|
+
return history[-limit:] if len(history) > limit else history
|
|
114
|
+
|
|
115
|
+
def add_message(self, session_id: str, role: str, content: str, metadata: dict = None):
|
|
116
|
+
"""Add a message to conversation history."""
|
|
117
|
+
if session_id not in self._conversation_history:
|
|
118
|
+
self._conversation_history[session_id] = []
|
|
119
|
+
|
|
120
|
+
message = {
|
|
121
|
+
"role": role,
|
|
122
|
+
"content": content,
|
|
123
|
+
"timestamp": datetime.now().isoformat(),
|
|
124
|
+
**(metadata or {})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
self._conversation_history[session_id].append(message)
|
|
128
|
+
|
|
129
|
+
# Trim to max size
|
|
130
|
+
max_history = 100
|
|
131
|
+
if len(self._conversation_history[session_id]) > max_history:
|
|
132
|
+
self._conversation_history[session_id] = self._conversation_history[session_id][-max_history:]
|
|
133
|
+
|
|
134
|
+
async def save_conversation(self, session_id: str):
|
|
135
|
+
"""Persist conversation to storage. Messages holding connector content
|
|
136
|
+
carry taint.TAINT_KEY so a reloaded conversation is re-flagged."""
|
|
137
|
+
history = taint.tag_messages(self._conversation_history.get(session_id, []))
|
|
138
|
+
if not history:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if self.db_pool:
|
|
142
|
+
await self._save_conversation_db(session_id, history)
|
|
143
|
+
else:
|
|
144
|
+
await self._save_conversation_file(session_id, history)
|
|
145
|
+
|
|
146
|
+
async def _save_conversation_db(self, session_id: str, history: list[dict]):
|
|
147
|
+
"""Save conversation to database."""
|
|
148
|
+
try:
|
|
149
|
+
async with self.db_pool.acquire() as conn:
|
|
150
|
+
# Get or create conversation
|
|
151
|
+
conv = await conn.fetchrow(
|
|
152
|
+
"""
|
|
153
|
+
INSERT INTO conversations (session_id, channel, last_message_at)
|
|
154
|
+
VALUES ($1, 'api', NOW())
|
|
155
|
+
ON CONFLICT (session_id) DO UPDATE SET last_message_at = NOW()
|
|
156
|
+
RETURNING id
|
|
157
|
+
""",
|
|
158
|
+
session_id
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Insert messages
|
|
162
|
+
for msg in history[-10:]: # Only save recent messages
|
|
163
|
+
await conn.execute(
|
|
164
|
+
"""
|
|
165
|
+
INSERT INTO messages (conversation_id, role, content, created_at, metadata)
|
|
166
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
167
|
+
""",
|
|
168
|
+
conv["id"],
|
|
169
|
+
msg["role"],
|
|
170
|
+
msg["content"],
|
|
171
|
+
datetime.fromisoformat(msg["timestamp"]),
|
|
172
|
+
json.dumps({taint.TAINT_KEY: msg[taint.TAINT_KEY]}
|
|
173
|
+
if msg.get(taint.TAINT_KEY) else {}),
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Failed to save conversation to DB: {e}")
|
|
177
|
+
|
|
178
|
+
async def _save_conversation_file(self, session_id: str, history: list[dict]):
|
|
179
|
+
"""Save conversation to file."""
|
|
180
|
+
try:
|
|
181
|
+
filepath = MEMORY_PATH / "conversations" / f"{session_id}.json"
|
|
182
|
+
filepath.parent.mkdir(exist_ok=True)
|
|
183
|
+
with open(filepath, 'w') as f:
|
|
184
|
+
json.dump(history, f, indent=2)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Failed to save conversation to file: {e}")
|
|
187
|
+
|
|
188
|
+
async def load_conversation(self, session_id: str) -> list[dict]:
|
|
189
|
+
"""Load conversation from storage."""
|
|
190
|
+
if self.db_pool:
|
|
191
|
+
return await self._load_conversation_db(session_id)
|
|
192
|
+
else:
|
|
193
|
+
return await self._load_conversation_file(session_id)
|
|
194
|
+
|
|
195
|
+
async def _load_conversation_db(self, session_id: str) -> list[dict]:
|
|
196
|
+
"""Load conversation from database."""
|
|
197
|
+
try:
|
|
198
|
+
async with self.db_pool.acquire() as conn:
|
|
199
|
+
rows = await conn.fetch(
|
|
200
|
+
"""
|
|
201
|
+
SELECT m.role, m.content, m.created_at, m.metadata
|
|
202
|
+
FROM messages m
|
|
203
|
+
JOIN conversations c ON m.conversation_id = c.id
|
|
204
|
+
WHERE c.session_id = $1
|
|
205
|
+
ORDER BY m.created_at DESC
|
|
206
|
+
LIMIT 50
|
|
207
|
+
""",
|
|
208
|
+
session_id
|
|
209
|
+
)
|
|
210
|
+
history = []
|
|
211
|
+
for r in reversed(rows):
|
|
212
|
+
msg = {"role": r["role"], "content": r["content"],
|
|
213
|
+
"timestamp": r["created_at"].isoformat()}
|
|
214
|
+
meta = r["metadata"]
|
|
215
|
+
if isinstance(meta, str):
|
|
216
|
+
meta = json.loads(meta or "{}")
|
|
217
|
+
if isinstance(meta, dict) and meta.get(taint.TAINT_KEY):
|
|
218
|
+
msg[taint.TAINT_KEY] = meta[taint.TAINT_KEY]
|
|
219
|
+
history.append(msg)
|
|
220
|
+
history = taint.restore_messages(session_id, history)
|
|
221
|
+
self._conversation_history[session_id] = history
|
|
222
|
+
return history
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Failed to load conversation from DB: {e}")
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
async def _load_conversation_file(self, session_id: str) -> list[dict]:
|
|
228
|
+
"""Load conversation from file."""
|
|
229
|
+
try:
|
|
230
|
+
filepath = MEMORY_PATH / "conversations" / f"{session_id}.json"
|
|
231
|
+
if filepath.exists():
|
|
232
|
+
with open(filepath) as f:
|
|
233
|
+
history = json.load(f)
|
|
234
|
+
history = taint.restore_messages(session_id, history)
|
|
235
|
+
self._conversation_history[session_id] = history
|
|
236
|
+
return history
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Failed to load conversation from file: {e}")
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
# =========================================================================
|
|
242
|
+
# MEMORY OPERATIONS
|
|
243
|
+
# =========================================================================
|
|
244
|
+
|
|
245
|
+
@training_sink("memory_manager.remember", own_words=True,
|
|
246
|
+
own_words_unless=lambda a: a.get("source") == "auto_store")
|
|
247
|
+
async def remember(
|
|
248
|
+
self,
|
|
249
|
+
content: str,
|
|
250
|
+
memory_type: str = "episode",
|
|
251
|
+
domain: str = None,
|
|
252
|
+
importance: float = 0.5,
|
|
253
|
+
is_core: bool = False,
|
|
254
|
+
metadata: dict = None,
|
|
255
|
+
source: str = "conversation",
|
|
256
|
+
) -> str:
|
|
257
|
+
"""Store a new memory. A memory the user asks for in their own words
|
|
258
|
+
saves; connector content is refused (taint.TaintedInput says why).
|
|
259
|
+
source="auto_store" (saved without the user asking) is also refused
|
|
260
|
+
in a conversation that read connector content."""
|
|
261
|
+
import uuid
|
|
262
|
+
|
|
263
|
+
memory = Memory(
|
|
264
|
+
id=str(uuid.uuid4()),
|
|
265
|
+
content=content,
|
|
266
|
+
memory_type=memory_type,
|
|
267
|
+
domain=domain,
|
|
268
|
+
importance=importance,
|
|
269
|
+
is_core=is_core,
|
|
270
|
+
metadata=metadata or {},
|
|
271
|
+
source=source,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if self.db_pool:
|
|
275
|
+
await self._store_memory_db(memory)
|
|
276
|
+
else:
|
|
277
|
+
await self._store_memory_file(memory)
|
|
278
|
+
|
|
279
|
+
return memory.id
|
|
280
|
+
|
|
281
|
+
@training_sink("memory_manager.store_memory_db", own_words=True)
|
|
282
|
+
async def _store_memory_db(self, memory: Memory):
|
|
283
|
+
"""Store memory in database."""
|
|
284
|
+
try:
|
|
285
|
+
# Generate embedding if we have a client
|
|
286
|
+
embedding = None
|
|
287
|
+
if self._embeddings_client and memory.content:
|
|
288
|
+
embedding = await self._get_embedding(memory.content)
|
|
289
|
+
|
|
290
|
+
async with self.db_pool.acquire() as conn:
|
|
291
|
+
await conn.execute(
|
|
292
|
+
"""
|
|
293
|
+
INSERT INTO memories (id, memory_type, content, summary, domain,
|
|
294
|
+
importance, embedding, source, is_core, metadata, created_at)
|
|
295
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
296
|
+
""",
|
|
297
|
+
memory.id,
|
|
298
|
+
memory.memory_type,
|
|
299
|
+
memory.content,
|
|
300
|
+
memory.summary,
|
|
301
|
+
memory.domain,
|
|
302
|
+
memory.importance,
|
|
303
|
+
embedding,
|
|
304
|
+
memory.source,
|
|
305
|
+
memory.is_core,
|
|
306
|
+
json.dumps(memory.metadata),
|
|
307
|
+
memory.created_at,
|
|
308
|
+
)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to store memory in DB: {e}")
|
|
311
|
+
# Fallback to file
|
|
312
|
+
await self._store_memory_file(memory)
|
|
313
|
+
|
|
314
|
+
async def _store_memory_file(self, memory: Memory):
|
|
315
|
+
"""Store memory in file."""
|
|
316
|
+
try:
|
|
317
|
+
# Append to daily log
|
|
318
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
319
|
+
daily_file = MEMORY_PATH / "daily" / f"{today}.md"
|
|
320
|
+
|
|
321
|
+
with open(daily_file, "a", encoding="utf-8") as f:
|
|
322
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
323
|
+
f.write(f"\n- [{timestamp}] [{memory.memory_type}] {memory.content}\n")
|
|
324
|
+
|
|
325
|
+
# If core, also append to core-episodes
|
|
326
|
+
if memory.is_core:
|
|
327
|
+
episodes_file = MEMORY_PATH / "core-episodes.md"
|
|
328
|
+
with open(episodes_file, "a", encoding="utf-8") as f:
|
|
329
|
+
date = datetime.now().strftime("%Y-%m-%d")
|
|
330
|
+
f.write(f"\n- [{date}] {memory.content}\n")
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.error(f"Failed to store memory to file: {e}")
|
|
334
|
+
|
|
335
|
+
async def recall(
|
|
336
|
+
self,
|
|
337
|
+
query: str,
|
|
338
|
+
limit: int = 5,
|
|
339
|
+
memory_type: str = None,
|
|
340
|
+
domain: str = None,
|
|
341
|
+
min_importance: float = 0.0
|
|
342
|
+
) -> list[Memory]:
|
|
343
|
+
"""Recall relevant memories."""
|
|
344
|
+
if self.db_pool:
|
|
345
|
+
return await self._recall_db(query, limit, memory_type, domain, min_importance)
|
|
346
|
+
else:
|
|
347
|
+
return await self._recall_file(query, limit, memory_type, domain)
|
|
348
|
+
|
|
349
|
+
async def _recall_db(
|
|
350
|
+
self,
|
|
351
|
+
query: str,
|
|
352
|
+
limit: int,
|
|
353
|
+
memory_type: str,
|
|
354
|
+
domain: str,
|
|
355
|
+
min_importance: float
|
|
356
|
+
) -> list[Memory]:
|
|
357
|
+
"""Recall from database with semantic search."""
|
|
358
|
+
try:
|
|
359
|
+
# Get embedding for query
|
|
360
|
+
query_embedding = None
|
|
361
|
+
if self._embeddings_client:
|
|
362
|
+
query_embedding = await self._get_embedding(query)
|
|
363
|
+
|
|
364
|
+
async with self.db_pool.acquire() as conn:
|
|
365
|
+
if query_embedding:
|
|
366
|
+
# Semantic search
|
|
367
|
+
rows = await conn.fetch(
|
|
368
|
+
"""
|
|
369
|
+
SELECT id, content, summary, memory_type, domain, importance,
|
|
370
|
+
1 - (embedding <=> $1::vector) as similarity
|
|
371
|
+
FROM memories
|
|
372
|
+
WHERE embedding IS NOT NULL
|
|
373
|
+
AND importance >= $2
|
|
374
|
+
AND ($3::text IS NULL OR memory_type = $3)
|
|
375
|
+
AND ($4::text IS NULL OR domain = $4)
|
|
376
|
+
ORDER BY embedding <=> $1::vector
|
|
377
|
+
LIMIT $5
|
|
378
|
+
""",
|
|
379
|
+
query_embedding,
|
|
380
|
+
min_importance,
|
|
381
|
+
memory_type,
|
|
382
|
+
domain,
|
|
383
|
+
limit
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
# Fallback to text search
|
|
387
|
+
rows = await conn.fetch(
|
|
388
|
+
"""
|
|
389
|
+
SELECT id, content, summary, memory_type, domain, importance, 0.5 as similarity
|
|
390
|
+
FROM memories
|
|
391
|
+
WHERE content ILIKE $1
|
|
392
|
+
AND importance >= $2
|
|
393
|
+
AND ($3::text IS NULL OR memory_type = $3)
|
|
394
|
+
AND ($4::text IS NULL OR domain = $4)
|
|
395
|
+
ORDER BY importance DESC, created_at DESC
|
|
396
|
+
LIMIT $5
|
|
397
|
+
""",
|
|
398
|
+
f"%{query}%",
|
|
399
|
+
min_importance,
|
|
400
|
+
memory_type,
|
|
401
|
+
domain,
|
|
402
|
+
limit
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return [
|
|
406
|
+
Memory(
|
|
407
|
+
id=str(r["id"]),
|
|
408
|
+
content=r["content"],
|
|
409
|
+
memory_type=r["memory_type"],
|
|
410
|
+
domain=r["domain"],
|
|
411
|
+
importance=r["importance"],
|
|
412
|
+
summary=r["summary"],
|
|
413
|
+
metadata={"similarity": r["similarity"]}
|
|
414
|
+
)
|
|
415
|
+
for r in rows
|
|
416
|
+
]
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.error(f"Failed to recall from DB: {e}")
|
|
419
|
+
return await self._recall_file(query, limit, memory_type, domain)
|
|
420
|
+
|
|
421
|
+
async def _recall_file(
|
|
422
|
+
self,
|
|
423
|
+
query: str,
|
|
424
|
+
limit: int,
|
|
425
|
+
memory_type: str,
|
|
426
|
+
domain: str
|
|
427
|
+
) -> list[Memory]:
|
|
428
|
+
"""Recall from file cache with keyword matching."""
|
|
429
|
+
results = []
|
|
430
|
+
query_lower = query.lower()
|
|
431
|
+
query_words = set(query_lower.split())
|
|
432
|
+
|
|
433
|
+
for cache_type, memories in self._file_cache.items():
|
|
434
|
+
for memory in memories:
|
|
435
|
+
if memory_type and memory.memory_type != memory_type:
|
|
436
|
+
continue
|
|
437
|
+
if domain and memory.domain != domain:
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
# Simple relevance scoring
|
|
441
|
+
content_lower = memory.content.lower()
|
|
442
|
+
matches = sum(1 for word in query_words if word in content_lower)
|
|
443
|
+
|
|
444
|
+
if matches > 0:
|
|
445
|
+
memory.metadata["score"] = matches / len(query_words)
|
|
446
|
+
results.append(memory)
|
|
447
|
+
|
|
448
|
+
# Sort by score and return top results
|
|
449
|
+
results.sort(key=lambda m: m.metadata.get("score", 0), reverse=True)
|
|
450
|
+
return results[:limit]
|
|
451
|
+
|
|
452
|
+
async def _get_embedding(self, text: str) -> list[float]:
|
|
453
|
+
"""Get embedding for text."""
|
|
454
|
+
# This would use OpenAI or Anthropic embeddings API
|
|
455
|
+
# For now, return None to use text-based search
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
# =========================================================================
|
|
459
|
+
# CONTEXT LOADING
|
|
460
|
+
# =========================================================================
|
|
461
|
+
|
|
462
|
+
def get_system_context(self) -> str:
|
|
463
|
+
"""Get full system context from workspace files."""
|
|
464
|
+
context_parts = []
|
|
465
|
+
|
|
466
|
+
# Load workspace files
|
|
467
|
+
files_to_load = [
|
|
468
|
+
("SOUL.md", WORKSPACE_PATH / "SOUL.md"),
|
|
469
|
+
("IDENTITY.md", WORKSPACE_PATH / "IDENTITY.md"),
|
|
470
|
+
("USER.md", WORKSPACE_PATH / "USER.md"),
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
for name, path in files_to_load:
|
|
474
|
+
if path.exists():
|
|
475
|
+
try:
|
|
476
|
+
content = path.read_text(encoding="utf-8")
|
|
477
|
+
context_parts.append(f"## {name}\n{content}\n")
|
|
478
|
+
except Exception as e:
|
|
479
|
+
logger.warning(f"Failed to load {name}: {e}")
|
|
480
|
+
|
|
481
|
+
# Add instincts
|
|
482
|
+
instincts = self._file_cache.get("instincts", [])
|
|
483
|
+
if instincts:
|
|
484
|
+
context_parts.append("## Behavioral Instincts")
|
|
485
|
+
for inst in instincts[:20]:
|
|
486
|
+
context_parts.append(f"- {inst.content}")
|
|
487
|
+
context_parts.append("")
|
|
488
|
+
|
|
489
|
+
# Add recent core episodes
|
|
490
|
+
episodes = self._file_cache.get("episodes", [])
|
|
491
|
+
if episodes:
|
|
492
|
+
context_parts.append("## Key Facts (Core Memory)")
|
|
493
|
+
for ep in episodes[-10:]:
|
|
494
|
+
context_parts.append(f"- {ep.content}")
|
|
495
|
+
context_parts.append("")
|
|
496
|
+
|
|
497
|
+
# Add current date
|
|
498
|
+
context_parts.append(f"\nCurrent date/time: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
|
|
499
|
+
|
|
500
|
+
return "\n".join(context_parts)
|
|
501
|
+
|
|
502
|
+
def get_working_memory(self) -> str:
|
|
503
|
+
"""Get current working memory content."""
|
|
504
|
+
memory_file = WORKSPACE_PATH / "MEMORY.md"
|
|
505
|
+
if memory_file.exists():
|
|
506
|
+
return memory_file.read_text(encoding="utf-8")
|
|
507
|
+
return ""
|
|
508
|
+
|
|
509
|
+
async def update_working_memory(self, section: str, content: str):
|
|
510
|
+
"""Update a section in working memory."""
|
|
511
|
+
memory_file = WORKSPACE_PATH / "MEMORY.md"
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
if memory_file.exists():
|
|
515
|
+
current = memory_file.read_text(encoding="utf-8")
|
|
516
|
+
else:
|
|
517
|
+
current = "# MEMORY.md - Working Memory\n"
|
|
518
|
+
|
|
519
|
+
# Update or append section
|
|
520
|
+
section_pattern = rf"(## {re.escape(section)}.*?)(?=\n## |\Z)"
|
|
521
|
+
new_section = f"## {section}\n{content}\n"
|
|
522
|
+
|
|
523
|
+
if re.search(section_pattern, current, re.DOTALL):
|
|
524
|
+
updated = re.sub(section_pattern, new_section, current, flags=re.DOTALL)
|
|
525
|
+
else:
|
|
526
|
+
updated = current.rstrip() + f"\n\n{new_section}"
|
|
527
|
+
|
|
528
|
+
# Add timestamp
|
|
529
|
+
updated = re.sub(
|
|
530
|
+
r"\*Last updated:.*\*",
|
|
531
|
+
f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*",
|
|
532
|
+
updated
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
memory_file.write_text(updated, encoding="utf-8")
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.error(f"Failed to update working memory: {e}")
|
|
539
|
+
|
|
540
|
+
# =========================================================================
|
|
541
|
+
# DAILY LOG
|
|
542
|
+
# =========================================================================
|
|
543
|
+
|
|
544
|
+
async def log_to_daily(self, entry: str, category: str = "note"):
|
|
545
|
+
"""Add entry to today's daily log."""
|
|
546
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
547
|
+
daily_file = MEMORY_PATH / "daily" / f"{today}.md"
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
if not daily_file.exists():
|
|
551
|
+
daily_file.parent.mkdir(exist_ok=True)
|
|
552
|
+
daily_file.write_text(f"# Daily Log - {today}\n\n")
|
|
553
|
+
|
|
554
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
555
|
+
with open(daily_file, "a", encoding="utf-8") as f:
|
|
556
|
+
f.write(f"- [{timestamp}] [{category}] {entry}\n")
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Failed to write daily log: {e}")
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# ============================================================================
|
|
563
|
+
# SINGLETON
|
|
564
|
+
# ============================================================================
|
|
565
|
+
|
|
566
|
+
_memory_manager: Optional[MemoryManager] = None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
async def get_memory_manager(db_pool=None) -> MemoryManager:
|
|
570
|
+
"""Get or create memory manager singleton."""
|
|
571
|
+
global _memory_manager
|
|
572
|
+
if _memory_manager is None:
|
|
573
|
+
_memory_manager = MemoryManager(db_pool)
|
|
574
|
+
await _memory_manager.initialize()
|
|
575
|
+
return _memory_manager
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def get_memory_manager_sync() -> MemoryManager:
|
|
579
|
+
"""Synchronous getter - assumes already initialized."""
|
|
580
|
+
global _memory_manager
|
|
581
|
+
if _memory_manager is None:
|
|
582
|
+
_memory_manager = MemoryManager()
|
|
583
|
+
asyncio.run(_memory_manager.initialize())
|
|
584
|
+
return _memory_manager
|