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/solomon.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperClaw Chat Agent
|
|
3
|
+
Simple chat interface used by Telegram bot and scheduler (chat-only, no tools).
|
|
4
|
+
Every prompt is built for a chat scope (hyperclaw.chat_scope): GUEST unless
|
|
5
|
+
the caller proves the chat is the owner's.
|
|
6
|
+
Users can customize the AI name and personality via workspace files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import asyncio
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncIterator, Optional
|
|
15
|
+
|
|
16
|
+
import anthropic
|
|
17
|
+
from hyperclaw.api_utils import extract_text
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
|
|
20
|
+
from hyperclaw import cost_guard
|
|
21
|
+
from hyperclaw.chat_scope import GUEST, GUEST_RULES, OWNER
|
|
22
|
+
from hyperclaw.cost_guard import guard_client
|
|
23
|
+
from hyperclaw.chat_scope import normalize as normalize_scope
|
|
24
|
+
|
|
25
|
+
_OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "")
|
|
26
|
+
_USE_OPENAI_COMPAT = bool(_OPENAI_API_BASE)
|
|
27
|
+
|
|
28
|
+
load_dotenv()
|
|
29
|
+
|
|
30
|
+
# Paths - use environment variable or default to ~/.hyperclaw
|
|
31
|
+
HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
|
|
32
|
+
WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
|
|
33
|
+
MEMORY_PATH = HYPERCLAW_ROOT / "memory"
|
|
34
|
+
|
|
35
|
+
MODEL = os.environ.get("OPENAI_MODEL") or os.environ.get("HYPERCLAW_MODEL", "claude-sonnet-4-6")
|
|
36
|
+
# Legacy override for the chat-reply max output; the per-path setting is
|
|
37
|
+
# config.json cost_guard.max_output_tokens_per_path["telegram"] (default 4096).
|
|
38
|
+
MAX_TOKENS = int(os.environ.get("HYPERCLAW_MAX_TOKENS", 4096))
|
|
39
|
+
MAX_HISTORY = 20
|
|
40
|
+
MAX_OUTPUT_PATH = "telegram"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def chat_max_tokens(provider: str, model: str) -> int:
|
|
44
|
+
"""The explicit max output for one chat reply (cost guard: every path sets
|
|
45
|
+
one, within the per-task cap for this model)."""
|
|
46
|
+
env = os.environ.get("HYPERCLAW_MAX_TOKENS", "").strip()
|
|
47
|
+
return cost_guard.max_output(MAX_OUTPUT_PATH, provider, model,
|
|
48
|
+
requested=int(env) if env.isdigit() else None)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def chat_thinking_on() -> bool:
|
|
52
|
+
"""Every chat answer streams its reasoning, Ask included (Vaughn, 7 Oct 2026,
|
|
53
|
+
item 12: "My live-reasoning standard applies to every answer, Ask included").
|
|
54
|
+
HYPERCLAW_CHAT_THINKING=off is the switch that turns it off."""
|
|
55
|
+
return os.environ.get("HYPERCLAW_CHAT_THINKING", "on").strip().lower() != "off"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def chat_request_kwargs(prov_kind: str, model: str, method=None, provider: str = "") -> dict:
|
|
59
|
+
"""Request kwargs for one chat turn on this provider.
|
|
60
|
+
|
|
61
|
+
Messages-API providers: max_tokens plus the model's thinking parameters from
|
|
62
|
+
anthropic_params (the one place that decides them), at low effort for these
|
|
63
|
+
Ask-style chat answers; with the switch off, max_tokens only.
|
|
64
|
+
OpenAI-compatible providers (HyperSpeed V2 on vLLM reasons by default): nothing
|
|
65
|
+
extra while on; with the switch off, the chat-template switch that stops it.
|
|
66
|
+
Hosted OpenAI-schema providers ("openai") get max_tokens only.
|
|
67
|
+
"""
|
|
68
|
+
mx = chat_max_tokens(provider or prov_kind, model)
|
|
69
|
+
if prov_kind == "anthropic":
|
|
70
|
+
from hyperclaw.anthropic_params import as_sdk_kwargs, thinking_params
|
|
71
|
+
if not chat_thinking_on():
|
|
72
|
+
return {"max_tokens": mx}
|
|
73
|
+
params = thinking_params(model, mx, overrides={"effort": "low"})
|
|
74
|
+
return as_sdk_kwargs(params, method) if method is not None else params
|
|
75
|
+
kwargs: dict = {"max_tokens": mx}
|
|
76
|
+
if prov_kind == "openai_compat" and not chat_thinking_on():
|
|
77
|
+
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
|
|
78
|
+
return kwargs
|
|
79
|
+
|
|
80
|
+
# Context files to load (in order, skip if missing)
|
|
81
|
+
CONTEXT_FILES = [
|
|
82
|
+
"ASSISTANT.md", # AI personality and name
|
|
83
|
+
"USER.md", # User preferences
|
|
84
|
+
"MEMORY.md", # Working memory
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_identity() -> tuple:
|
|
89
|
+
"""(ai_name, user_name) — onboarding writes ~/.hyperclaw/config.json;
|
|
90
|
+
older builds used config/settings.json. Check both, in that order."""
|
|
91
|
+
import json
|
|
92
|
+
for cf in (HYPERCLAW_ROOT / "config.json",
|
|
93
|
+
HYPERCLAW_ROOT / "config" / "settings.json"):
|
|
94
|
+
if cf.exists():
|
|
95
|
+
try:
|
|
96
|
+
config = json.loads(cf.read_text())
|
|
97
|
+
return (config.get("ai_name") or os.environ.get("HYPERCLAW_AI_NAME", "Assistant"),
|
|
98
|
+
config.get("user_name") or "")
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
return (os.environ.get("HYPERCLAW_AI_NAME", "Assistant"), "")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_ai_name() -> str:
|
|
105
|
+
return _get_identity()[0]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _model_family(model: str) -> str:
|
|
109
|
+
"""Human-readable family for a model id, or "" if unknown."""
|
|
110
|
+
m = model.lower()
|
|
111
|
+
if "hyperspeed" in m or m.startswith("hyper-nimbus/"):
|
|
112
|
+
return "HyperAI"
|
|
113
|
+
if "claude" in m:
|
|
114
|
+
return "Claude"
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ChatAgent:
|
|
119
|
+
"""Chat agent - manages conversation and context."""
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
if _USE_OPENAI_COMPAT:
|
|
123
|
+
import openai
|
|
124
|
+
self._oai = guard_client(openai.AsyncOpenAI(
|
|
125
|
+
api_key=os.environ.get("OPENAI_API_KEY", ""),
|
|
126
|
+
base_url=_OPENAI_API_BASE,
|
|
127
|
+
))
|
|
128
|
+
self.client = None
|
|
129
|
+
else:
|
|
130
|
+
self._oai = None
|
|
131
|
+
self.client = guard_client(
|
|
132
|
+
anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
|
|
133
|
+
self.ai_name, self.user_name = _get_identity()
|
|
134
|
+
self.system_prompt = self._load_system_prompt()
|
|
135
|
+
|
|
136
|
+
def _load_system_prompt(self, model: str = "", scope: str = GUEST) -> str:
|
|
137
|
+
"""Build the system prompt (hyperclaw.prompt_assembly order).
|
|
138
|
+
|
|
139
|
+
model: the model id actually serving the conversation; defaults to
|
|
140
|
+
the configured MODEL so callers routing to another provider can
|
|
141
|
+
rebuild the prompt with the real model.
|
|
142
|
+
scope: chat_scope.OWNER or GUEST. Anything but OWNER gets the guest
|
|
143
|
+
prompt: no persona, no standing instructions, no USER/MEMORY files,
|
|
144
|
+
no daily log, no owner name (spec §6 guest fence; benchmark 4.1).
|
|
145
|
+
Truth rules and the access manifest always come first (spec §14)."""
|
|
146
|
+
from hyperclaw.prompt_assembly import assemble, identity_block, persona_for
|
|
147
|
+
from hyperclaw.truth_guard import TRUTH_RULES, access_manifest
|
|
148
|
+
|
|
149
|
+
model = model or MODEL
|
|
150
|
+
scope = normalize_scope(scope)
|
|
151
|
+
owner = scope == OWNER
|
|
152
|
+
identity = identity_block(self.ai_name, self.user_name if owner else "",
|
|
153
|
+
scope, model=model, family=_model_family(model))
|
|
154
|
+
surface = "\n".join([
|
|
155
|
+
"## Core Behaviors",
|
|
156
|
+
"- Be helpful, accurate, and concise",
|
|
157
|
+
"- Execute tasks proactively when given clear instructions",
|
|
158
|
+
"- Ask clarifying questions when needed",
|
|
159
|
+
"- Be honest about limitations",
|
|
160
|
+
"",
|
|
161
|
+
f"Current date: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
162
|
+
])
|
|
163
|
+
access = access_manifest("chat_only", surface="telegram", scope=scope)
|
|
164
|
+
if not owner:
|
|
165
|
+
# Guest fence: nothing from the owner's workspace or memory.
|
|
166
|
+
return assemble(truth_rules=TRUTH_RULES, access=access, identity=identity,
|
|
167
|
+
scope=scope, surface_instructions=surface)
|
|
168
|
+
|
|
169
|
+
# Legacy persona file from older onboarding: kept as the owner's
|
|
170
|
+
# standing instructions, below the truth rules.
|
|
171
|
+
standing = ""
|
|
172
|
+
claude_md = HYPERCLAW_ROOT / "CLAUDE.md"
|
|
173
|
+
if claude_md.exists():
|
|
174
|
+
try:
|
|
175
|
+
standing = claude_md.read_text(encoding="utf-8")
|
|
176
|
+
except Exception:
|
|
177
|
+
standing = ""
|
|
178
|
+
|
|
179
|
+
context = []
|
|
180
|
+
for filename in CONTEXT_FILES:
|
|
181
|
+
filepath = WORKSPACE_PATH / filename
|
|
182
|
+
if filepath.exists():
|
|
183
|
+
try:
|
|
184
|
+
content = filepath.read_text(encoding="utf-8")
|
|
185
|
+
context.append(f"## {filename}\n{content}")
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
190
|
+
log_path = MEMORY_PATH / f"{today}.md"
|
|
191
|
+
if log_path.exists():
|
|
192
|
+
try:
|
|
193
|
+
content = log_path.read_text(encoding="utf-8")
|
|
194
|
+
context.append(f"## Today's Log ({today})\n{content}")
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
return assemble(truth_rules=TRUTH_RULES, access=access, identity=identity,
|
|
199
|
+
scope=scope, surface_instructions=surface,
|
|
200
|
+
persona=persona_for(scope), standing=standing, context=context)
|
|
201
|
+
|
|
202
|
+
async def chat(self, message: str, history: list[dict], scope: str = GUEST,
|
|
203
|
+
unattended: Optional[bool] = None) -> str:
|
|
204
|
+
"""Send a message and get a response. scope defaults to GUEST
|
|
205
|
+
(fail closed); owner-only callers such as the scheduler pass OWNER.
|
|
206
|
+
|
|
207
|
+
Goes through the provider registry like stream_events, so the
|
|
208
|
+
scheduler works on a HyperSpeed V2-only install and honours a
|
|
209
|
+
Hyper-deployed install's V2-only rule.
|
|
210
|
+
|
|
211
|
+
unattended: a job with nobody waiting on the answer (the scheduler).
|
|
212
|
+
None means "owner scope with no conversation", which is how the
|
|
213
|
+
scheduler calls this. At a cost cap an unattended job stops and
|
|
214
|
+
returns a notice for the owner only (scheduler output goes only to
|
|
215
|
+
the owner's chat); it never gets a cheaper model."""
|
|
216
|
+
if unattended is None:
|
|
217
|
+
unattended = normalize_scope(scope) == OWNER and not history
|
|
218
|
+
out = []
|
|
219
|
+
try:
|
|
220
|
+
with cost_guard.task(surface="scheduler" if unattended else "chat", request=message,
|
|
221
|
+
unattended=unattended, owner=normalize_scope(scope) == OWNER):
|
|
222
|
+
async for kind, chunk in self.stream_events(message, history, scope=scope):
|
|
223
|
+
if kind == "text":
|
|
224
|
+
out.append(chunk)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
# In-band marker: consumers strip it (strip_internal_errors) and
|
|
227
|
+
# show a notices.py failure notice; the detail is logged here.
|
|
228
|
+
logging.getLogger("hyperclaw.solomon").error("chat failed: %s", e, exc_info=True)
|
|
229
|
+
return f"[Error: {e}]"
|
|
230
|
+
return "".join(out)
|
|
231
|
+
|
|
232
|
+
async def _attempt_stream(self, prov, model, messages,
|
|
233
|
+
scope: str = GUEST) -> AsyncIterator[tuple]:
|
|
234
|
+
"""One provider attempt for stream_events — yields ("thinking"|"text", delta)."""
|
|
235
|
+
system = self._load_system_prompt(model, scope=scope)
|
|
236
|
+
if prov.kind == "anthropic":
|
|
237
|
+
from hyperclaw.model_catalog import TELEGRAM, is_selected
|
|
238
|
+
if not is_selected(TELEGRAM, getattr(prov, "name", ""), model): # /model wins
|
|
239
|
+
model = os.environ.get("HYPERCLAW_MODEL", "").strip() or model
|
|
240
|
+
async_client = guard_client(anthropic.AsyncAnthropic(api_key=prov.api_key),
|
|
241
|
+
getattr(prov, "name", None))
|
|
242
|
+
async with async_client.messages.stream(
|
|
243
|
+
model=model,
|
|
244
|
+
system=system,
|
|
245
|
+
messages=messages,
|
|
246
|
+
**chat_request_kwargs(prov.kind, model, async_client.messages.stream,
|
|
247
|
+
provider=getattr(prov, "name", "") or ""),
|
|
248
|
+
) as stream:
|
|
249
|
+
async for event in stream:
|
|
250
|
+
if event.type == "content_block_delta":
|
|
251
|
+
dtype = getattr(event.delta, "type", "")
|
|
252
|
+
if dtype == "thinking_delta":
|
|
253
|
+
yield ("thinking", event.delta.thinking)
|
|
254
|
+
elif dtype == "text_delta":
|
|
255
|
+
yield ("text", event.delta.text)
|
|
256
|
+
return
|
|
257
|
+
# openai_compat / openai: chat-completions schema, text content only
|
|
258
|
+
from openai import AsyncOpenAI
|
|
259
|
+
client = guard_client(AsyncOpenAI(api_key=prov.api_key, base_url=prov.base_url or None),
|
|
260
|
+
getattr(prov, "name", None))
|
|
261
|
+
oai = [{"role": "system", "content": system}]
|
|
262
|
+
for m in messages:
|
|
263
|
+
c = m.get("content")
|
|
264
|
+
if isinstance(c, list):
|
|
265
|
+
c = "\n".join(b.get("text", "") for b in c
|
|
266
|
+
if isinstance(b, dict) and b.get("type") == "text")
|
|
267
|
+
if c:
|
|
268
|
+
oai.append({"role": m["role"], "content": c})
|
|
269
|
+
from hyperclaw.xai import clean_request
|
|
270
|
+
stream = await client.chat.completions.create(
|
|
271
|
+
model=model, messages=oai, stream=True,
|
|
272
|
+
**clean_request(getattr(prov, "name", ""), getattr(prov, "base_url", ""), model,
|
|
273
|
+
chat_request_kwargs(prov.kind, model,
|
|
274
|
+
provider=getattr(prov, "name", "") or "")),
|
|
275
|
+
)
|
|
276
|
+
# Reasoning (vLLM reasoning_content / inline <think>) -> "thinking",
|
|
277
|
+
# answer -> "text" with its leading blank lines dropped.
|
|
278
|
+
from hyperclaw.oai_stream import DeltaSplitter
|
|
279
|
+
splitter = DeltaSplitter()
|
|
280
|
+
async for chunk in stream:
|
|
281
|
+
if not chunk.choices:
|
|
282
|
+
continue # e.g. the trailing usage-only chunk
|
|
283
|
+
# reasoning_content / reasoning -> ("thinking", ...); content -> ("text", ...)
|
|
284
|
+
for item in splitter.feed(chunk.choices[0].delta):
|
|
285
|
+
yield item
|
|
286
|
+
for item in splitter.flush():
|
|
287
|
+
yield item
|
|
288
|
+
|
|
289
|
+
async def stream_events(self, message: str, history: list[dict],
|
|
290
|
+
attachments: list | None = None,
|
|
291
|
+
scope: str = GUEST, _unattended: bool = False) -> AsyncIterator[tuple]:
|
|
292
|
+
"""Stream ("thinking", delta) and ("text", delta) tuples.
|
|
293
|
+
|
|
294
|
+
Routes by capability: plain text turns use the `primary` slot,
|
|
295
|
+
attachment turns need documents/images and use `vision`. Failover
|
|
296
|
+
walks the slot ladder before the first token; a mid-stream death is
|
|
297
|
+
marked, never re-answered.
|
|
298
|
+
|
|
299
|
+
One call is one task for the cost guard (spec §9). At a cap no model
|
|
300
|
+
call is made: the owner gets the stop-and-ask message, a guest a
|
|
301
|
+
neutral reply (the owner is notified), an unattended job a notice
|
|
302
|
+
for the owner. Owner answers above the threshold end with a spend line.
|
|
303
|
+
"""
|
|
304
|
+
owner = normalize_scope(scope) == OWNER
|
|
305
|
+
with cost_guard.task(surface="chat", request=message, owner=owner,
|
|
306
|
+
unattended=_unattended) as task:
|
|
307
|
+
if owner and not task.unattended:
|
|
308
|
+
notices = cost_guard.take_owner_notices()
|
|
309
|
+
if notices:
|
|
310
|
+
yield ("text", "\n".join(notices) + "\n\n")
|
|
311
|
+
answered = False
|
|
312
|
+
try:
|
|
313
|
+
async for item in self._stream_events(message, history, attachments, scope):
|
|
314
|
+
answered = answered or item[0] == "text"
|
|
315
|
+
yield item
|
|
316
|
+
except client_guard_blocked() as blocked:
|
|
317
|
+
# The final check stopped an outside-model request: nothing was sent.
|
|
318
|
+
yield ("text", ("\n\n" if answered else "") + blocked.user_message)
|
|
319
|
+
return
|
|
320
|
+
except cost_guard.CostCapReached as cap:
|
|
321
|
+
if task.unattended:
|
|
322
|
+
yield ("text", cost_guard.unattended_message(cap))
|
|
323
|
+
else:
|
|
324
|
+
yield ("text", ("\n\n" if answered else "")
|
|
325
|
+
+ cost_guard.stop_message(cap, owner=owner))
|
|
326
|
+
return
|
|
327
|
+
# Restricted-client guard: say plainly what was left out (and that
|
|
328
|
+
# HyperSpeed V2 includes it), on every answer that withheld something.
|
|
329
|
+
from hyperclaw import client_guard
|
|
330
|
+
note = client_guard.reply_note(task)
|
|
331
|
+
if note:
|
|
332
|
+
yield ("text", "\n\n" + note)
|
|
333
|
+
if owner and answered and not task.unattended:
|
|
334
|
+
line = cost_guard.spend_line(task)
|
|
335
|
+
if line:
|
|
336
|
+
yield ("text", "\n\n" + line)
|
|
337
|
+
|
|
338
|
+
async def _stream_events(self, message: str, history: list[dict],
|
|
339
|
+
attachments: list | None = None,
|
|
340
|
+
scope: str = GUEST) -> AsyncIterator[tuple]:
|
|
341
|
+
from hyperclaw.providers import registry, stream_with_failover
|
|
342
|
+
|
|
343
|
+
messages = self._prepare_messages(message, history, attachments)
|
|
344
|
+
kinds = {b.get("type") for b in (attachments or []) if isinstance(b, dict)}
|
|
345
|
+
if kinds & {"document", "image"}:
|
|
346
|
+
# Native file blocks need a provider that reads them. Text-only
|
|
347
|
+
# attachments (inlined files, locally extracted PDF text) ride
|
|
348
|
+
# the primary slot like any chat turn — HyperSpeed V2 included.
|
|
349
|
+
need = {"chat"} | ({"documents"} if "document" in kinds else set()) \
|
|
350
|
+
| ({"images"} if "image" in kinds else set())
|
|
351
|
+
slot = "vision"
|
|
352
|
+
else:
|
|
353
|
+
slot, need = "primary", {"chat", "streaming"}
|
|
354
|
+
# /model on Telegram (docs/MODEL-SWITCHING.md): the chosen model first.
|
|
355
|
+
from hyperclaw.model_catalog import TELEGRAM, prefer_selected
|
|
356
|
+
reg = registry()
|
|
357
|
+
candidates = prefer_selected(reg, TELEGRAM, reg.resolve(slot, need), need)
|
|
358
|
+
# No automatic fallback to a third-party provider without the owner's
|
|
359
|
+
# consent (model_policy); the allowed list already filtered resolve().
|
|
360
|
+
from hyperclaw.model_policy import fallback_ladder
|
|
361
|
+
candidates = fallback_ladder(candidates)
|
|
362
|
+
if not candidates:
|
|
363
|
+
yield ("text", f"[Error: no configured provider supports this turn ({slot})]")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
async def attempt(prov, model):
|
|
367
|
+
async for item in self._attempt_stream(prov, model, messages, scope=scope):
|
|
368
|
+
yield item
|
|
369
|
+
|
|
370
|
+
async for item in stream_with_failover(candidates, attempt):
|
|
371
|
+
yield item
|
|
372
|
+
|
|
373
|
+
async def stream_chat(self, message: str, history: list[dict],
|
|
374
|
+
scope: str = GUEST) -> AsyncIterator[str]:
|
|
375
|
+
"""Stream a response token by token."""
|
|
376
|
+
messages = self._prepare_messages(message, history)
|
|
377
|
+
system_prompt = self._load_system_prompt(scope=scope)
|
|
378
|
+
try:
|
|
379
|
+
if _USE_OPENAI_COMPAT:
|
|
380
|
+
stream = await self._oai.chat.completions.create(
|
|
381
|
+
model=MODEL,
|
|
382
|
+
max_tokens=chat_max_tokens("openai", MODEL),
|
|
383
|
+
messages=[{"role": "system", "content": system_prompt}] + messages,
|
|
384
|
+
stream=True,
|
|
385
|
+
)
|
|
386
|
+
async for chunk in stream:
|
|
387
|
+
delta = chunk.choices[0].delta.content
|
|
388
|
+
if delta:
|
|
389
|
+
yield delta
|
|
390
|
+
else:
|
|
391
|
+
async_client = guard_client(anthropic.AsyncAnthropic(
|
|
392
|
+
api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
|
|
393
|
+
async with async_client.messages.stream(
|
|
394
|
+
model=MODEL,
|
|
395
|
+
max_tokens=chat_max_tokens("anthropic", MODEL),
|
|
396
|
+
system=system_prompt,
|
|
397
|
+
messages=messages,
|
|
398
|
+
) as stream:
|
|
399
|
+
async for text in stream.text_stream:
|
|
400
|
+
yield text
|
|
401
|
+
except Exception as e:
|
|
402
|
+
yield f"[Error: {e}]"
|
|
403
|
+
|
|
404
|
+
def _prepare_messages(self, message: str, history: list[dict],
|
|
405
|
+
attachments: list | None = None) -> list[dict]:
|
|
406
|
+
"""Prepare messages for the API call, trimming to MAX_HISTORY.
|
|
407
|
+
|
|
408
|
+
attachments: optional Anthropic content blocks (document / image)
|
|
409
|
+
that ride along with the user's text — how Telegram file uploads
|
|
410
|
+
reach the model."""
|
|
411
|
+
recent = history[-MAX_HISTORY:] if len(history) > MAX_HISTORY else history
|
|
412
|
+
if attachments:
|
|
413
|
+
content = list(attachments) + [{"type": "text", "text": message or "(see attached)"}]
|
|
414
|
+
return list(recent) + [{"role": "user", "content": content}]
|
|
415
|
+
return list(recent) + [{"role": "user", "content": message}]
|
|
416
|
+
|
|
417
|
+
def reload_context(self) -> None:
|
|
418
|
+
"""Reload the system prompt (hot-reload without restart)."""
|
|
419
|
+
self.ai_name = _get_ai_name()
|
|
420
|
+
self.system_prompt = self._load_system_prompt()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# Singleton instance
|
|
424
|
+
_agent: Optional[ChatAgent] = None
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_chat_agent() -> ChatAgent:
|
|
428
|
+
"""Get or create the chat agent singleton."""
|
|
429
|
+
global _agent
|
|
430
|
+
if _agent is None:
|
|
431
|
+
_agent = ChatAgent()
|
|
432
|
+
return _agent
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# Backwards compatibility aliases
|
|
436
|
+
Solomon = ChatAgent
|
|
437
|
+
get_solomon = get_chat_agent
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def client_guard_blocked():
|
|
441
|
+
from hyperclaw.client_guard import RestrictedContentBlocked
|
|
442
|
+
return RestrictedContentBlocked
|