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
hyperclaw/cli_tui.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
HyperClaw Terminal UI — Production-ready with signal handling and smart model routing.
|
|
4
|
+
Features:
|
|
5
|
+
- Ctrl+C to interrupt streaming
|
|
6
|
+
- Automatic model selection (cheap for simple, powerful for complex)
|
|
7
|
+
- Persistent memory across sessions
|
|
8
|
+
- Out-of-box setup verification
|
|
9
|
+
- Better error recovery
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import threading
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
# Ensure we can import hyperclaw modules
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
25
|
+
|
|
26
|
+
from dotenv import load_dotenv
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# CONFIGURATION
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
|
|
33
|
+
WORKSPACE = HYPERCLAW_ROOT / "workspace"
|
|
34
|
+
MEMORY_DIR = HYPERCLAW_ROOT / "memory"
|
|
35
|
+
SECRETS_DIR = WORKSPACE / "secrets"
|
|
36
|
+
SESSION_FILE = HYPERCLAW_ROOT / "session_history.json"
|
|
37
|
+
CONFIG_FILE = HYPERCLAW_ROOT / "config.json"
|
|
38
|
+
|
|
39
|
+
# Load environment
|
|
40
|
+
env_path = SECRETS_DIR / ".env"
|
|
41
|
+
if env_path.exists():
|
|
42
|
+
load_dotenv(env_path)
|
|
43
|
+
else:
|
|
44
|
+
load_dotenv()
|
|
45
|
+
from hyperclaw.teamsetup.secrets import export_env as _export_secrets # noqa: E402
|
|
46
|
+
_export_secrets() # team installs keep keys in the OS secret store
|
|
47
|
+
|
|
48
|
+
# Terminal colors
|
|
49
|
+
RESET = "\033[0m"
|
|
50
|
+
BOLD = "\033[1m"
|
|
51
|
+
DIM = "\033[2m"
|
|
52
|
+
RED = "\033[31m"
|
|
53
|
+
GREEN = "\033[32m"
|
|
54
|
+
YELLOW = "\033[33m"
|
|
55
|
+
BLUE = "\033[34m"
|
|
56
|
+
MAGENTA = "\033[35m"
|
|
57
|
+
CYAN = "\033[36m"
|
|
58
|
+
|
|
59
|
+
# ============================================================================
|
|
60
|
+
# SIGNAL HANDLING
|
|
61
|
+
# ============================================================================
|
|
62
|
+
|
|
63
|
+
STOP_STREAMING = False
|
|
64
|
+
STREAMING_ACTIVE = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def signal_handler(signum, frame):
|
|
68
|
+
"""Handle Ctrl+C to interrupt streaming."""
|
|
69
|
+
global STOP_STREAMING
|
|
70
|
+
if STREAMING_ACTIVE:
|
|
71
|
+
STOP_STREAMING = True
|
|
72
|
+
print(f"\n{YELLOW}[Interrupting...]{RESET}", flush=True)
|
|
73
|
+
else:
|
|
74
|
+
print(f"\n{DIM}Use /quit to exit{RESET}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Install signal handler
|
|
78
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================================
|
|
82
|
+
# CONFIGURATION LOADING
|
|
83
|
+
# ============================================================================
|
|
84
|
+
|
|
85
|
+
def load_config() -> dict:
|
|
86
|
+
"""Load config with AI/user names."""
|
|
87
|
+
if CONFIG_FILE.exists():
|
|
88
|
+
try:
|
|
89
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return {"user_name": "User", "ai_name": "HyperClaw"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
CONFIG = load_config()
|
|
96
|
+
AI_NAME = CONFIG.get("ai_name", "HyperClaw")
|
|
97
|
+
USER_NAME = CONFIG.get("user_name", "User")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_setup() -> dict:
|
|
101
|
+
"""Check if HyperClaw is set up properly."""
|
|
102
|
+
checks = {
|
|
103
|
+
"api_key": bool(os.environ.get("ANTHROPIC_API_KEY")),
|
|
104
|
+
"workspace": WORKSPACE.exists(),
|
|
105
|
+
"memory": MEMORY_DIR.exists(),
|
|
106
|
+
"chatjimmy": bool(os.environ.get("CHATJIMMY_API_KEY")),
|
|
107
|
+
"database": bool(os.environ.get("DATABASE_URL")),
|
|
108
|
+
}
|
|
109
|
+
return checks
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ensure_setup():
|
|
113
|
+
"""Ensure directories exist."""
|
|
114
|
+
WORKSPACE.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
(MEMORY_DIR / "daily").mkdir(exist_ok=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ============================================================================
|
|
120
|
+
# SESSION MANAGEMENT
|
|
121
|
+
# ============================================================================
|
|
122
|
+
|
|
123
|
+
def load_session() -> list:
|
|
124
|
+
"""Load previous session history."""
|
|
125
|
+
if SESSION_FILE.exists():
|
|
126
|
+
try:
|
|
127
|
+
data = json.loads(SESSION_FILE.read_text())
|
|
128
|
+
return data[-30:] if len(data) > 30 else data
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_session(history: list):
|
|
135
|
+
"""Save session history."""
|
|
136
|
+
try:
|
|
137
|
+
to_save = history[-50:] if len(history) > 50 else history
|
|
138
|
+
SESSION_FILE.write_text(json.dumps(to_save, indent=2))
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def load_memory_context() -> str:
|
|
144
|
+
"""Load memory files for context."""
|
|
145
|
+
parts = []
|
|
146
|
+
|
|
147
|
+
files = [
|
|
148
|
+
WORKSPACE / "SOUL.md",
|
|
149
|
+
WORKSPACE / "IDENTITY.md",
|
|
150
|
+
WORKSPACE / "USER.md",
|
|
151
|
+
WORKSPACE / "MEMORY.md",
|
|
152
|
+
MEMORY_DIR / "instincts.md",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
for f in files:
|
|
156
|
+
if f.exists():
|
|
157
|
+
try:
|
|
158
|
+
content = f.read_text()[:3000]
|
|
159
|
+
parts.append(f"## {f.name}\n{content}")
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# Today's log
|
|
164
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
165
|
+
today_log = MEMORY_DIR / "daily" / f"{today}.md"
|
|
166
|
+
if today_log.exists():
|
|
167
|
+
try:
|
|
168
|
+
content = today_log.read_text()[-2000:]
|
|
169
|
+
parts.append(f"## Today's Log\n{content}")
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return "\n\n".join(parts)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# CHAT ENGINE
|
|
178
|
+
# ============================================================================
|
|
179
|
+
|
|
180
|
+
class TUIChat:
|
|
181
|
+
"""Terminal UI Chat with smart routing and signal handling."""
|
|
182
|
+
|
|
183
|
+
def __init__(self):
|
|
184
|
+
self.history = []
|
|
185
|
+
self.model_router = None
|
|
186
|
+
self.memory_manager = None
|
|
187
|
+
self._initialized = False
|
|
188
|
+
|
|
189
|
+
async def initialize(self):
|
|
190
|
+
"""Initialize components."""
|
|
191
|
+
if self._initialized:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
from hyperclaw.model_router import get_model_router
|
|
196
|
+
self.model_router = get_model_router()
|
|
197
|
+
except ImportError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
from hyperclaw.memory_manager import get_memory_manager
|
|
202
|
+
self.memory_manager = await get_memory_manager()
|
|
203
|
+
except ImportError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
self._initialized = True
|
|
207
|
+
|
|
208
|
+
def build_system_prompt(self) -> str:
|
|
209
|
+
"""hyperclaw.prompt_assembly order (spec §14): truth rules and access
|
|
210
|
+
first; this chat runs no tools; memory is fenced as DATA (bench 1.6)."""
|
|
211
|
+
from hyperclaw.chat_scope import OWNER
|
|
212
|
+
from hyperclaw.prompt_assembly import assemble, identity_block, persona_for
|
|
213
|
+
from hyperclaw.truth_guard import TRUTH_RULES, access_manifest
|
|
214
|
+
surface = "\n".join([
|
|
215
|
+
"## How to work",
|
|
216
|
+
"- Be concise but thorough when needed.",
|
|
217
|
+
"",
|
|
218
|
+
f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
219
|
+
])
|
|
220
|
+
memory_context = load_memory_context()
|
|
221
|
+
return assemble(truth_rules=TRUTH_RULES, access=access_manifest("chat_only"),
|
|
222
|
+
identity=identity_block(AI_NAME, "", OWNER), scope=OWNER,
|
|
223
|
+
surface_instructions=surface, persona=persona_for(OWNER),
|
|
224
|
+
context=[("memory", memory_context)] if memory_context else [])
|
|
225
|
+
|
|
226
|
+
async def chat(self, message: str, stream: bool = True) -> str:
|
|
227
|
+
"""Send a message and get response with streaming."""
|
|
228
|
+
global STOP_STREAMING, STREAMING_ACTIVE
|
|
229
|
+
|
|
230
|
+
if not self._initialized:
|
|
231
|
+
await self.initialize()
|
|
232
|
+
|
|
233
|
+
# Classify complexity for model selection
|
|
234
|
+
is_simple = self._is_simple_query(message)
|
|
235
|
+
|
|
236
|
+
# Select model based on complexity
|
|
237
|
+
if is_simple and self.model_router:
|
|
238
|
+
model_id = "chatjimmy" if os.environ.get("CHATJIMMY_API_KEY") else "claude-haiku"
|
|
239
|
+
model_name = "ChatJimmy" if model_id == "chatjimmy" else "Haiku"
|
|
240
|
+
else:
|
|
241
|
+
model_id = "claude-sonnet"
|
|
242
|
+
model_name = "Sonnet"
|
|
243
|
+
|
|
244
|
+
print(f"{DIM}[{model_name}]{RESET} ", end="", flush=True)
|
|
245
|
+
|
|
246
|
+
# Add to history
|
|
247
|
+
self.history.append({"role": "user", "content": message})
|
|
248
|
+
|
|
249
|
+
system = self.build_system_prompt()
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
if self.model_router and not stream:
|
|
253
|
+
# Use model router for non-streaming
|
|
254
|
+
response, metadata = await self.model_router.call(
|
|
255
|
+
message=message,
|
|
256
|
+
system=system,
|
|
257
|
+
history=self.history[:-1],
|
|
258
|
+
)
|
|
259
|
+
self.history.append({"role": "assistant", "content": response})
|
|
260
|
+
return response
|
|
261
|
+
else:
|
|
262
|
+
# Direct streaming with Anthropic
|
|
263
|
+
return await self._stream_anthropic(message, system, model_id)
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
error_msg = f"[Error: {str(e)[:100]}]"
|
|
267
|
+
print(f"{RED}{error_msg}{RESET}")
|
|
268
|
+
return error_msg
|
|
269
|
+
|
|
270
|
+
async def _stream_anthropic(self, message: str, system: str, model_id: str) -> str:
|
|
271
|
+
"""Stream response from Anthropic API with interrupt handling."""
|
|
272
|
+
global STOP_STREAMING, STREAMING_ACTIVE
|
|
273
|
+
import anthropic
|
|
274
|
+
|
|
275
|
+
STOP_STREAMING = False
|
|
276
|
+
STREAMING_ACTIVE = True
|
|
277
|
+
|
|
278
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
279
|
+
if not api_key:
|
|
280
|
+
return "[Error: ANTHROPIC_API_KEY not set]"
|
|
281
|
+
|
|
282
|
+
from hyperclaw.cost_guard import guard_client
|
|
283
|
+
client = guard_client(anthropic.Anthropic(api_key=api_key), "anthropic")
|
|
284
|
+
|
|
285
|
+
# Map model_id to actual model
|
|
286
|
+
from hyperclaw.providers import registry
|
|
287
|
+
_anth = registry().providers.get("anthropic")
|
|
288
|
+
_m = _anth.models if _anth else {}
|
|
289
|
+
model_map = {
|
|
290
|
+
"chatjimmy": _m.get("fast", "claude-haiku-4-5-20251001"), # fallback if ChatJimmy unavailable
|
|
291
|
+
"claude-haiku": _m.get("fast", "claude-haiku-4-5-20251001"),
|
|
292
|
+
"claude-sonnet": _m.get("default", "claude-sonnet-4-6"),
|
|
293
|
+
"claude-opus": _m.get("strong", "claude-opus-4-8"),
|
|
294
|
+
}
|
|
295
|
+
model = model_map.get(model_id, _m.get("default", "claude-sonnet-4-6"))
|
|
296
|
+
|
|
297
|
+
full_response = []
|
|
298
|
+
start_time = time.time()
|
|
299
|
+
max_time = 120 # 2 minute timeout
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
from hyperclaw.cost_guard import max_output
|
|
303
|
+
with client.messages.stream(
|
|
304
|
+
model=model,
|
|
305
|
+
max_tokens=max_output("chat", "anthropic", model),
|
|
306
|
+
system=system,
|
|
307
|
+
messages=self.history,
|
|
308
|
+
) as stream:
|
|
309
|
+
print(f"{MAGENTA}{BOLD}{AI_NAME}:{RESET} ", end="", flush=True)
|
|
310
|
+
|
|
311
|
+
for text in stream.text_stream:
|
|
312
|
+
# Check for interrupt
|
|
313
|
+
if STOP_STREAMING:
|
|
314
|
+
print(f"\n{YELLOW}[Stopped]{RESET}")
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
# Check timeout
|
|
318
|
+
if time.time() - start_time > max_time:
|
|
319
|
+
print(f"\n{YELLOW}[Timeout]{RESET}")
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
full_response.append(text)
|
|
323
|
+
print(text, end="", flush=True)
|
|
324
|
+
|
|
325
|
+
print() # Newline at end
|
|
326
|
+
|
|
327
|
+
except anthropic.APIError as e:
|
|
328
|
+
print(f"\n{RED}API Error: {e}{RESET}")
|
|
329
|
+
except Exception as e:
|
|
330
|
+
print(f"\n{RED}Error: {e}{RESET}")
|
|
331
|
+
finally:
|
|
332
|
+
STREAMING_ACTIVE = False
|
|
333
|
+
|
|
334
|
+
response_text = "".join(full_response)
|
|
335
|
+
if response_text:
|
|
336
|
+
self.history.append({"role": "assistant", "content": response_text})
|
|
337
|
+
|
|
338
|
+
return response_text
|
|
339
|
+
|
|
340
|
+
def _is_simple_query(self, message: str) -> bool:
|
|
341
|
+
"""Check if query is simple enough for fast model."""
|
|
342
|
+
message_lower = message.lower().strip()
|
|
343
|
+
|
|
344
|
+
# Very short messages
|
|
345
|
+
if len(message) < 30:
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
# Simple patterns
|
|
349
|
+
simple_patterns = [
|
|
350
|
+
"hi", "hello", "hey", "thanks", "ok", "yes", "no",
|
|
351
|
+
"what time", "what date", "what day",
|
|
352
|
+
"list", "show", "get", "check",
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
for pattern in simple_patterns:
|
|
356
|
+
if message_lower.startswith(pattern):
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
def reset(self):
|
|
362
|
+
"""Reset conversation history."""
|
|
363
|
+
self.history = []
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ============================================================================
|
|
367
|
+
# SLASH COMMANDS
|
|
368
|
+
# ============================================================================
|
|
369
|
+
|
|
370
|
+
def handle_command(cmd: str, chat: TUIChat) -> bool:
|
|
371
|
+
"""Handle slash commands. Returns True if should continue loop."""
|
|
372
|
+
cmd = cmd.lower().strip()
|
|
373
|
+
|
|
374
|
+
if cmd in ("/quit", "/exit", "/q"):
|
|
375
|
+
save_session(chat.history)
|
|
376
|
+
print(f"{DIM}Goodbye.{RESET}\n")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
elif cmd == "/reset":
|
|
380
|
+
chat.reset()
|
|
381
|
+
save_session([])
|
|
382
|
+
print(f"{GREEN}Conversation reset.{RESET}\n")
|
|
383
|
+
|
|
384
|
+
elif cmd == "/status":
|
|
385
|
+
checks = check_setup()
|
|
386
|
+
print(f"\n{BOLD}HyperClaw Status{RESET}")
|
|
387
|
+
print(f" API Key: {'OK' if checks['api_key'] else 'Missing'}")
|
|
388
|
+
print(f" ChatJimmy: {'OK' if checks['chatjimmy'] else 'Not configured (optional)'}")
|
|
389
|
+
print(f" Database: {'OK' if checks['database'] else 'Not configured (optional)'}")
|
|
390
|
+
print(f" History: {len(chat.history)} messages")
|
|
391
|
+
print()
|
|
392
|
+
|
|
393
|
+
elif cmd == "/models":
|
|
394
|
+
print(f"\n{BOLD}Available Models{RESET}")
|
|
395
|
+
print(f" ChatJimmy - Simple queries (cheapest)")
|
|
396
|
+
print(f" Claude Haiku - Fast tasks")
|
|
397
|
+
print(f" Claude Sonnet - Complex tasks (default)")
|
|
398
|
+
print(f" Claude Opus - Deep reasoning")
|
|
399
|
+
print()
|
|
400
|
+
|
|
401
|
+
elif cmd == "/save":
|
|
402
|
+
save_session(chat.history)
|
|
403
|
+
print(f"{GREEN}Session saved.{RESET}\n")
|
|
404
|
+
|
|
405
|
+
elif cmd == "/help":
|
|
406
|
+
print(f"\n{BOLD}Commands{RESET}")
|
|
407
|
+
print(f" /reset - Clear conversation")
|
|
408
|
+
print(f" /status - Show system status")
|
|
409
|
+
print(f" /models - List available models")
|
|
410
|
+
print(f" /save - Save session")
|
|
411
|
+
print(f" /help - Show this help")
|
|
412
|
+
print(f" /quit - Exit")
|
|
413
|
+
print()
|
|
414
|
+
print(f"{BOLD}Tips{RESET}")
|
|
415
|
+
print(f" - Press Ctrl+C to interrupt streaming")
|
|
416
|
+
print(f" - Simple queries use cheaper models automatically")
|
|
417
|
+
print()
|
|
418
|
+
|
|
419
|
+
else:
|
|
420
|
+
print(f"{YELLOW}Unknown command: {cmd}{RESET}")
|
|
421
|
+
print(f"{DIM}Type /help for available commands{RESET}\n")
|
|
422
|
+
|
|
423
|
+
return True
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ============================================================================
|
|
427
|
+
# MAIN
|
|
428
|
+
# ============================================================================
|
|
429
|
+
|
|
430
|
+
async def main_async():
|
|
431
|
+
"""Async main loop."""
|
|
432
|
+
# Check setup
|
|
433
|
+
checks = check_setup()
|
|
434
|
+
|
|
435
|
+
if not checks["api_key"]:
|
|
436
|
+
print(f"\n{RED}ANTHROPIC_API_KEY not set.{RESET}")
|
|
437
|
+
print(f"{DIM}Run 'hyperclaw setup' or set the environment variable.{RESET}\n")
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Ensure directories
|
|
441
|
+
ensure_setup()
|
|
442
|
+
|
|
443
|
+
# Initialize chat
|
|
444
|
+
chat = TUIChat()
|
|
445
|
+
await chat.initialize()
|
|
446
|
+
|
|
447
|
+
# Load previous session
|
|
448
|
+
chat.history = load_session()
|
|
449
|
+
session_msg = f"Resumed ({len(chat.history)} msgs)" if chat.history else "New session"
|
|
450
|
+
|
|
451
|
+
# Welcome message
|
|
452
|
+
print(f"\n{CYAN}{BOLD}=== HyperClaw ==={RESET}")
|
|
453
|
+
print(f"{DIM}AI Assistant with smart model routing{RESET}")
|
|
454
|
+
print(f"{DIM}{session_msg} | Ctrl+C to stop | /help for commands{RESET}")
|
|
455
|
+
|
|
456
|
+
if checks["chatjimmy"]:
|
|
457
|
+
print(f"{DIM}ChatJimmy enabled for cost savings{RESET}")
|
|
458
|
+
|
|
459
|
+
print()
|
|
460
|
+
|
|
461
|
+
# Main loop
|
|
462
|
+
while True:
|
|
463
|
+
try:
|
|
464
|
+
user_input = input(f"{CYAN}>{RESET} ").strip()
|
|
465
|
+
|
|
466
|
+
if not user_input:
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# Handle commands
|
|
470
|
+
if user_input.startswith("/"):
|
|
471
|
+
if not handle_command(user_input, chat):
|
|
472
|
+
break
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Chat
|
|
476
|
+
await chat.chat(user_input)
|
|
477
|
+
save_session(chat.history)
|
|
478
|
+
print()
|
|
479
|
+
|
|
480
|
+
except KeyboardInterrupt:
|
|
481
|
+
if not STREAMING_ACTIVE:
|
|
482
|
+
print(f"\n{DIM}Use /quit to exit{RESET}")
|
|
483
|
+
except EOFError:
|
|
484
|
+
save_session(chat.history)
|
|
485
|
+
print(f"\n{DIM}Goodbye.{RESET}")
|
|
486
|
+
break
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def main():
|
|
490
|
+
"""Entry point."""
|
|
491
|
+
from hyperclaw.updates import on_startup
|
|
492
|
+
on_startup("tui")
|
|
493
|
+
asyncio.run(main_async())
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
if __name__ == "__main__":
|
|
497
|
+
main()
|