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.
Files changed (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,591 @@
1
+ """
2
+ HyperClaw Orchestrator
3
+ Central coordinator for all agents, integrations, memory, and model routing.
4
+ Production-ready with cost-optimized model selection and multi-agent coordination.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, AsyncIterator, Optional, Union
14
+
15
+ from dotenv import load_dotenv
16
+
17
+ from .memory_manager import MemoryManager, get_memory_manager
18
+ from .model_router import ModelRouter, ModelTier, get_model_router
19
+ from .agent_coordinator import AgentCoordinator, get_coordinator, Task
20
+ from .cost_guard import CostCapReached, stop_message
21
+
22
+ logger = logging.getLogger("hyperclaw.orchestrator")
23
+
24
+ # Load environment
25
+ load_dotenv()
26
+
27
+ # Configuration
28
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
29
+
30
+
31
+ class Orchestrator:
32
+ """
33
+ Central orchestrator for HyperClaw.
34
+ Manages agents, integrations, memory, and cost-optimized model routing.
35
+ """
36
+
37
+ def __init__(self):
38
+ self._initialized = False
39
+ self._model_router: Optional[ModelRouter] = None
40
+ self._coordinator: Optional[AgentCoordinator] = None
41
+ self._memory: Optional[MemoryManager] = None
42
+ self._db_pool = None
43
+ self._integrations: dict[str, Any] = {}
44
+ self._system_prompt: str = ""
45
+
46
+ async def initialize(self, db_pool=None):
47
+ """Initialize all components."""
48
+ if self._initialized:
49
+ return
50
+
51
+ logger.info("Initializing HyperClaw Orchestrator...")
52
+
53
+ # Database pool
54
+ self._db_pool = db_pool
55
+
56
+ # Initialize model router (handles Claude, ChatJimmy, etc.)
57
+ self._model_router = get_model_router()
58
+ logger.info(f"Model router initialized with {len(self._model_router.models)} models")
59
+
60
+ # Initialize agent coordinator
61
+ self._coordinator = await get_coordinator()
62
+ logger.info(f"Agent coordinator initialized with {len(self._coordinator.agents)} agents")
63
+
64
+ # Initialize memory manager
65
+ self._memory = await get_memory_manager(db_pool)
66
+ logger.info("Memory manager initialized")
67
+
68
+ # Load system prompt
69
+ self._system_prompt = self._build_system_prompt()
70
+
71
+ # Initialize integrations
72
+ await self._init_integrations()
73
+
74
+ # Start background workers for task processing
75
+ await self._coordinator.start_workers(num_workers=3)
76
+
77
+ self._initialized = True
78
+ logger.info("Orchestrator initialization complete")
79
+
80
+ @staticmethod
81
+ def _build_guest_system_prompt() -> str:
82
+ """Prompt for a non-owner chat: no workspace context, no memory."""
83
+ from .chat_scope import GUEST_RULES
84
+ return "\n".join([
85
+ "# HyperClaw AI Assistant", "",
86
+ "You are a helpful AI assistant.", "", GUEST_RULES, "",
87
+ f"Current date/time: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
88
+ ])
89
+
90
+ def _build_system_prompt(self) -> str:
91
+ """Build the (owner) system prompt from workspace context."""
92
+ parts = [
93
+ "# HyperClaw AI Assistant",
94
+ "",
95
+ "You are a sophisticated AI assistant with access to multiple specialized agents.",
96
+ "",
97
+ "## Core Behaviors",
98
+ "- Be proactive and execute tasks without unnecessary confirmation",
99
+ "- Be resourceful - try multiple approaches before asking for help",
100
+ "- Be concise but thorough when needed",
101
+ "- Maintain context across conversations using memory",
102
+ "- Route complex tasks to appropriate specialist agents",
103
+ "",
104
+ "## Available Agents",
105
+ ]
106
+
107
+ # List available agents
108
+ if self._coordinator:
109
+ for agent in list(self._coordinator.agents.values())[:10]:
110
+ parts.append(f"- {agent.name} ({agent.domain}): {agent.role}")
111
+
112
+ parts.append("")
113
+
114
+ # Add context from memory
115
+ if self._memory:
116
+ context = self._memory.get_system_context()
117
+ if context:
118
+ parts.append("## Workspace Context")
119
+ parts.append(context)
120
+ parts.append("")
121
+
122
+ parts.append(f"Current date/time: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
123
+
124
+ return "\n".join(parts)
125
+
126
+ async def _init_integrations(self):
127
+ """Initialize available integrations."""
128
+ # Telegram
129
+ if os.environ.get("TELEGRAM_BOT_TOKEN"):
130
+ self._integrations["telegram"] = {"status": "configured", "type": "bot"}
131
+ logger.info("Telegram integration configured")
132
+
133
+ # Gmail
134
+ if os.environ.get("GMAIL_REFRESH_TOKEN"):
135
+ self._integrations["gmail"] = {"status": "configured", "type": "oauth2"}
136
+ logger.info("Gmail integration configured")
137
+
138
+ # ChatJimmy
139
+ if os.environ.get("CHATJIMMY_API_KEY"):
140
+ self._integrations["chatjimmy"] = {"status": "configured", "type": "api"}
141
+ logger.info("ChatJimmy integration configured")
142
+
143
+ # ElevenLabs
144
+ if os.environ.get("ELEVENLABS_API_KEY"):
145
+ self._integrations["elevenlabs"] = {"status": "configured", "type": "api"}
146
+ logger.info("ElevenLabs integration configured")
147
+
148
+ logger.info(f"Initialized {len(self._integrations)} integrations")
149
+
150
+ # =========================================================================
151
+ # CHAT INTERFACE (Cost-Optimized)
152
+ # =========================================================================
153
+
154
+ async def chat(
155
+ self,
156
+ message: str,
157
+ session_id: str = "default",
158
+ channel: str = "api",
159
+ stream: bool = False,
160
+ force_model: str = None,
161
+ scope: str = "owner",
162
+ ) -> Union[str, AsyncIterator[str]]:
163
+ """
164
+ Main chat interface with intelligent model routing.
165
+ Simple queries go to ChatJimmy, complex tasks to Claude.
166
+
167
+ scope: chat_scope.OWNER (the local API caller) or GUEST. Chat
168
+ channels must resolve it with chat_scope.scope_for(); a GUEST turn
169
+ gets no workspace context, no memory recall, and is not written
170
+ into memory (spec §6 guest fence).
171
+ """
172
+ from .chat_scope import OWNER, normalize
173
+ scope = normalize(scope)
174
+ if not self._initialized:
175
+ await self.initialize()
176
+
177
+ # Get conversation history
178
+ history = self._memory.get_conversation_history(session_id)
179
+
180
+ # Add user message to history
181
+ self._memory.add_message(session_id, "user", message, {"channel": channel})
182
+
183
+ remember = scope == OWNER
184
+ if remember:
185
+ # Recall relevant memories
186
+ relevant_memories = await self._memory.recall(message, limit=5)
187
+ memory_context = ""
188
+ if relevant_memories:
189
+ memory_context = "\n\n## Relevant Memories\n"
190
+ for mem in relevant_memories:
191
+ memory_context += f"- [{mem.memory_type}] {mem.content}\n"
192
+
193
+ # Build system prompt with memory context
194
+ system = self._system_prompt + memory_context
195
+ else:
196
+ system = self._build_guest_system_prompt()
197
+
198
+ # Prepare messages for API
199
+ messages = self._prepare_messages(message, history)
200
+
201
+ if stream:
202
+ return self._stream_response(messages, system, session_id, force_model,
203
+ remember=remember)
204
+ else:
205
+ return await self._get_response(messages, system, session_id, force_model,
206
+ remember=remember)
207
+
208
+ async def _get_response(
209
+ self,
210
+ messages: list[dict],
211
+ system: str,
212
+ session_id: str,
213
+ force_model: str = None,
214
+ remember: bool = True,
215
+ ) -> str:
216
+ """Get a response using cost-optimized model routing."""
217
+ user_message = messages[-1]["content"] if messages else ""
218
+
219
+ try:
220
+ # Use model router for intelligent selection
221
+ response, metadata = await self._model_router.call(
222
+ message=user_message,
223
+ system=system,
224
+ history=messages[:-1], # Exclude current message
225
+ model_override=force_model,
226
+ )
227
+
228
+ # Log model usage
229
+ model_used = metadata.get("model_name", "unknown")
230
+ tier_used = metadata.get("tier", "unknown")
231
+ logger.info(f"Response from {model_used} (tier={tier_used})")
232
+
233
+ # Store assistant message
234
+ self._memory.add_message(session_id, "assistant", response, {
235
+ "model": metadata.get("model"),
236
+ "tier": tier_used,
237
+ })
238
+
239
+ if remember:
240
+ # Auto-store significant exchanges
241
+ await self._auto_store_memory(user_message, response, session_id)
242
+
243
+ # Log to daily
244
+ await self._memory.log_to_daily(
245
+ f"Chat ({model_used}): {user_message[:80]}...",
246
+ "conversation"
247
+ )
248
+
249
+ return response
250
+
251
+ except CostCapReached as e: # at the cap: stop and ask, never a cheaper model
252
+ return stop_message(e)
253
+ except Exception as e:
254
+ logger.error(f"Chat error: {e}", exc_info=True)
255
+ return f"[Error: {e}]"
256
+
257
+ async def _stream_response(
258
+ self,
259
+ messages: list[dict],
260
+ system: str,
261
+ session_id: str,
262
+ force_model: str = None,
263
+ remember: bool = True,
264
+ ) -> AsyncIterator[str]:
265
+ """Stream a response (falls back to non-streaming for now)."""
266
+ # For streaming, we need to use Anthropic's streaming API directly
267
+ # The model router doesn't support streaming yet, so we'll use
268
+ # the standard model and stream from there
269
+ import anthropic
270
+
271
+ user_message = messages[-1]["content"] if messages else ""
272
+
273
+ # Determine which model to use
274
+ if force_model:
275
+ model_id = force_model
276
+ else:
277
+ model_config = self._model_router.select_model(user_message)
278
+ model_id = model_config.id
279
+
280
+ # Only Anthropic models support streaming currently
281
+ if not model_id.startswith("claude"):
282
+ # Fallback to non-streaming
283
+ response = await self._get_response(messages, system, session_id, force_model,
284
+ remember=remember)
285
+ yield response
286
+ return
287
+
288
+ full_response = []
289
+
290
+ try:
291
+ api_key = os.environ.get("ANTHROPIC_API_KEY", "")
292
+ from hyperclaw.cost_guard import guard_client, max_output
293
+ async_client = guard_client(anthropic.AsyncAnthropic(api_key=api_key), "anthropic")
294
+
295
+ async with async_client.messages.stream(
296
+ model=model_id,
297
+ max_tokens=max_output("chat", "anthropic", model_id),
298
+ system=system,
299
+ messages=messages,
300
+ ) as stream:
301
+ async for text in stream.text_stream:
302
+ full_response.append(text)
303
+ yield text
304
+
305
+ # Store complete response
306
+ response_text = "".join(full_response)
307
+ self._memory.add_message(session_id, "assistant", response_text)
308
+ if remember:
309
+ await self._auto_store_memory(user_message, response_text, session_id)
310
+
311
+ except CostCapReached as e: # at the cap: stop and ask, never a cheaper model
312
+ yield stop_message(e)
313
+ except Exception as e:
314
+ logger.error(f"Streaming error: {e}", exc_info=True)
315
+ yield f"[Error: {e}]"
316
+
317
+ def _prepare_messages(self, message: str, history: list[dict]) -> list[dict]:
318
+ """Prepare messages for API call."""
319
+ messages = []
320
+ for msg in history[-20:]:
321
+ messages.append({
322
+ "role": msg["role"],
323
+ "content": msg["content"]
324
+ })
325
+ messages.append({"role": "user", "content": message})
326
+ return messages
327
+
328
+ async def _auto_store_memory(self, user_msg: str, assistant_msg: str,
329
+ session_id: str = None):
330
+ """Automatically store significant exchanges as memories. Never in a
331
+ conversation that read connector content (hyperclaw.taint): the
332
+ refusal is logged and written to the activity log, nothing is saved."""
333
+ store_triggers = [
334
+ "remember", "note that", "important", "always", "never",
335
+ "decided", "approved", "confirmed", "scheduled", "completed",
336
+ ]
337
+
338
+ combined = (user_msg + " " + assistant_msg).lower()
339
+ should_store = any(trigger in combined for trigger in store_triggers)
340
+
341
+ if should_store:
342
+ summary = f"User: {user_msg[:150]}... | Response: {assistant_msg[:150]}..."
343
+ from hyperclaw import taint
344
+ try:
345
+ with taint.conversation(session_id):
346
+ await self._memory.remember(
347
+ content=summary,
348
+ memory_type="episode",
349
+ importance=0.7,
350
+ source="auto_store"
351
+ )
352
+ except taint.TaintedInput as e:
353
+ logger.info(f"auto-store skipped: {e.reason} ({e.label})")
354
+
355
+ # =========================================================================
356
+ # AGENT DISPATCH
357
+ # =========================================================================
358
+
359
+ async def dispatch_task(
360
+ self,
361
+ goal: str,
362
+ domain: str = None,
363
+ task_type: str = None,
364
+ agent_id: str = None,
365
+ priority: int = 5
366
+ ) -> Task:
367
+ """Dispatch a task to the agent coordinator."""
368
+ if not self._coordinator:
369
+ raise RuntimeError("Coordinator not initialized")
370
+
371
+ task = await self._coordinator.submit_task(
372
+ goal=goal,
373
+ domain=domain,
374
+ task_type=task_type,
375
+ agent_id=agent_id,
376
+ priority=priority,
377
+ )
378
+
379
+ return task
380
+
381
+ async def execute_task(self, task_id: str) -> str:
382
+ """Execute a specific task."""
383
+ if not self._coordinator:
384
+ raise RuntimeError("Coordinator not initialized")
385
+
386
+ task = self._coordinator.tasks.get(task_id)
387
+ if not task:
388
+ raise ValueError(f"Task {task_id} not found")
389
+
390
+ return await self._coordinator.execute_task(task)
391
+
392
+ async def coordinate_goal(self, goal: str, context: dict = None) -> dict:
393
+ """
394
+ Coordinate multiple agents to accomplish a complex goal.
395
+ """
396
+ if not self._coordinator:
397
+ raise RuntimeError("Coordinator not initialized")
398
+
399
+ return await self._coordinator.coordinate(goal, context)
400
+
401
+ # =========================================================================
402
+ # INTEGRATION METHODS
403
+ # =========================================================================
404
+
405
+ async def send_telegram(self, chat_id: str, message: str) -> bool:
406
+ """Send a Telegram message."""
407
+ if "telegram" not in self._integrations:
408
+ logger.warning("Telegram not configured")
409
+ return False
410
+
411
+ try:
412
+ import httpx
413
+ token = os.environ.get("TELEGRAM_BOT_TOKEN")
414
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
415
+
416
+ async with httpx.AsyncClient() as client:
417
+ from hyperclaw.tools.egress import telegram_payload
418
+ response = await client.post(url, json=telegram_payload({
419
+ "chat_id": chat_id,
420
+ "text": message,
421
+ "parse_mode": "Markdown"
422
+ }))
423
+ # True only with Telegram's receipt: ok=true AND a message_id (bench 20.3).
424
+ from hyperclaw.tools.receipts import telegram_receipt
425
+ mid, why = telegram_receipt(response)
426
+ if not mid:
427
+ logger.error(f"Telegram send failed: {why}")
428
+ return bool(mid)
429
+
430
+ except Exception as e:
431
+ from hyperclaw.tools.receipts import redact
432
+ logger.error(f"Telegram send failed: {redact(e)}")
433
+ return False
434
+
435
+ async def get_emails(self, limit: int = 10) -> list[dict]:
436
+ """Get recent emails."""
437
+ if "gmail" not in self._integrations:
438
+ return []
439
+ # Implementation would use Gmail API
440
+ return []
441
+
442
+ # =========================================================================
443
+ # STATUS & HEALTH
444
+ # =========================================================================
445
+
446
+ def get_status(self) -> dict:
447
+ """Get comprehensive system status."""
448
+ status = {
449
+ "initialized": self._initialized,
450
+ "timestamp": datetime.now().isoformat(),
451
+ }
452
+
453
+ # Model router stats
454
+ if self._model_router:
455
+ status["model_router"] = self._model_router.get_stats()
456
+
457
+ # Coordinator stats
458
+ if self._coordinator:
459
+ status["coordinator"] = self._coordinator.get_status()
460
+
461
+ # Memory stats
462
+ if self._memory:
463
+ status["memory"] = {
464
+ "conversations": len(self._memory._conversation_history),
465
+ "cached_memories": sum(
466
+ len(v) for v in self._memory._file_cache.values()
467
+ ),
468
+ }
469
+
470
+ # Integrations
471
+ status["integrations"] = {
472
+ name: info["status"]
473
+ for name, info in self._integrations.items()
474
+ }
475
+
476
+ # Database
477
+ status["database"] = "connected" if self._db_pool else "not configured"
478
+
479
+ return status
480
+
481
+ async def health_check(self) -> dict:
482
+ """Run health checks on all components."""
483
+ checks = {
484
+ "orchestrator": self._initialized,
485
+ "model_router": self._model_router is not None,
486
+ "coordinator": self._coordinator is not None,
487
+ "memory": self._memory is not None,
488
+ "database": False,
489
+ }
490
+
491
+ # Check database
492
+ if self._db_pool:
493
+ try:
494
+ async with self._db_pool.acquire() as conn:
495
+ await conn.execute("SELECT 1")
496
+ checks["database"] = True
497
+ except Exception:
498
+ pass
499
+
500
+ return {
501
+ "healthy": all(checks.values()),
502
+ "checks": checks,
503
+ "timestamp": datetime.now().isoformat(),
504
+ }
505
+
506
+ # =========================================================================
507
+ # MEMORY INTERFACE
508
+ # =========================================================================
509
+
510
+ async def remember(self, content: str, **kwargs) -> str:
511
+ """Store a memory explicitly."""
512
+ if not self._memory:
513
+ return ""
514
+ return await self._memory.remember(content, **kwargs)
515
+
516
+ async def recall(self, query: str, **kwargs) -> list:
517
+ """Recall relevant memories."""
518
+ if not self._memory:
519
+ return []
520
+ return await self._memory.recall(query, **kwargs)
521
+
522
+ async def save_session(self, session_id: str):
523
+ """Save current session to storage."""
524
+ if self._memory:
525
+ await self._memory.save_conversation(session_id)
526
+
527
+ # =========================================================================
528
+ # COST MANAGEMENT
529
+ # =========================================================================
530
+
531
+ def get_cost_stats(self) -> dict:
532
+ """Get cost statistics from model router."""
533
+ if not self._model_router:
534
+ return {}
535
+ return self._model_router.get_stats()
536
+
537
+ def set_daily_budget(self, budget_usd: float):
538
+ """Set the daily cost cap (an explicit user action through the API;
539
+ the cost guard records it in the activity log)."""
540
+ from hyperclaw import cost_guard
541
+ cost_guard.raise_cap(cost_guard.DAILY, budget_usd, by="API /api/costs/budget",
542
+ surface="api")
543
+ logger.info(f"Daily cost cap set to ${budget_usd}")
544
+
545
+ # =========================================================================
546
+ # CLEANUP
547
+ # =========================================================================
548
+
549
+ async def shutdown(self):
550
+ """Graceful shutdown."""
551
+ logger.info("Shutting down orchestrator...")
552
+
553
+ # Stop coordinator workers
554
+ if self._coordinator:
555
+ await self._coordinator.stop_workers()
556
+
557
+ # Save all conversations
558
+ if self._memory:
559
+ for session_id in list(self._memory._conversation_history.keys()):
560
+ await self._memory.save_conversation(session_id)
561
+
562
+ # Close database pool
563
+ if self._db_pool:
564
+ await self._db_pool.close()
565
+
566
+ logger.info("Orchestrator shutdown complete")
567
+
568
+
569
+ # ============================================================================
570
+ # SINGLETON
571
+ # ============================================================================
572
+
573
+ _orchestrator: Optional[Orchestrator] = None
574
+
575
+
576
+ async def get_orchestrator(db_pool=None) -> Orchestrator:
577
+ """Get or create orchestrator singleton."""
578
+ global _orchestrator
579
+ if _orchestrator is None:
580
+ _orchestrator = Orchestrator()
581
+ await _orchestrator.initialize(db_pool)
582
+ return _orchestrator
583
+
584
+
585
+ def get_orchestrator_sync() -> Orchestrator:
586
+ """Synchronous getter for orchestrator."""
587
+ global _orchestrator
588
+ if _orchestrator is None:
589
+ _orchestrator = Orchestrator()
590
+ asyncio.run(_orchestrator.initialize())
591
+ return _orchestrator
hyperclaw/outbox.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ Outbox — per-conversation outbound file queue.
3
+
4
+ Lets a tool (currently send_file / media_hub.deliver_file via='here') hand a file
5
+ to the CURRENT conversation channel without knowing which channel it is.
6
+ (Doc/chart engines do not auto-queue yet — the model must call send_file.) The channel adapter
7
+ (telegram_direct, imessage_daemon_v2, email) drains the queue after each turn
8
+ and delivers the files natively (sendDocument/sendPhoto, iMessage attachment,
9
+ email attachment).
10
+
11
+ Thread-safe: tui_bridge runs turns in a thread pool, so the "current session"
12
+ is tracked per-thread.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import threading
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Dict, List, Optional
21
+
22
+ _lock = threading.Lock()
23
+ _queues: Dict[int, List[dict]] = {} # chat_id -> [{path, caption, queued_at}]
24
+ _current = threading.local() # per-thread active chat_id
25
+
26
+ MAX_QUEUE_PER_CHAT = 50 # cap so an undrained chat can't grow unbounded
27
+ ENTRY_TTL_SECONDS = 900 # stale entries (>15 min) are dropped at drain time
28
+
29
+ IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".heic", ".bmp"}
30
+ VIDEO_EXTS = {".mp4", ".mov", ".m4v", ".avi", ".webm"}
31
+ AUDIO_EXTS = {".mp3", ".m4a", ".wav", ".ogg", ".flac", ".aiff"}
32
+
33
+
34
+ def set_current_session(chat_id: Optional[int]) -> None:
35
+ """Bind the running thread to a conversation (called by tui_bridge per turn)."""
36
+ _current.chat_id = chat_id
37
+
38
+
39
+ def get_current_session() -> Optional[int]:
40
+ return getattr(_current, "chat_id", None)
41
+
42
+
43
+ def queue_file(path: str, caption: str = "", chat_id: Optional[int] = None) -> str:
44
+ """Queue a file for delivery to a conversation. Returns a status string
45
+ (tool-friendly). Falls back to the thread's current session."""
46
+ cid = chat_id if chat_id is not None else get_current_session()
47
+ if cid is None:
48
+ return ("No active conversation to deliver to. Use send_file with via="
49
+ "'telegram'/'imessage'/'email' and an explicit recipient instead.")
50
+ p = Path(path).expanduser()
51
+ try:
52
+ size = p.stat().st_size
53
+ except OSError:
54
+ return f"File not found: {p}"
55
+ if size > 49 * 1024 * 1024:
56
+ return f"File too large for chat delivery ({size // (1024*1024)}MB > 49MB). Email it instead."
57
+ with _lock:
58
+ q = _queues.setdefault(cid, [])
59
+ q.append({"path": str(p), "caption": caption or "", "queued_at": time.time()})
60
+ if len(q) > MAX_QUEUE_PER_CHAT:
61
+ del q[:-MAX_QUEUE_PER_CHAT]
62
+ return f"Queued {p.name} for delivery in this conversation."
63
+
64
+
65
+ def drain(chat_id: int) -> List[dict]:
66
+ """Take and clear all queued files for a conversation (stale entries dropped)."""
67
+ with _lock:
68
+ entries = _queues.pop(chat_id, [])
69
+ cutoff = time.time() - ENTRY_TTL_SECONDS
70
+ return [e for e in entries if e.get("queued_at", cutoff + 1) > cutoff]
71
+
72
+
73
+ def kind_of(path: str) -> str:
74
+ """'photo' | 'video' | 'audio' | 'document' by extension."""
75
+ ext = Path(path).suffix.lower()
76
+ if ext in IMAGE_EXTS:
77
+ return "photo"
78
+ if ext in VIDEO_EXTS:
79
+ return "video"
80
+ if ext in AUDIO_EXTS:
81
+ return "audio"
82
+ return "document"