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/naming.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Naming the assistant: the first step of onboarding (spec §14).
|
|
2
|
+
|
|
3
|
+
The customer either types a name, or asks for a suggestion. A suggestion is
|
|
4
|
+
ONE name plus a one-sentence reason, generated by the configured model when
|
|
5
|
+
one is reachable, otherwise taken from a fixed starter list (deterministic
|
|
6
|
+
order). The customer keeps it, asks for another, or types their own. The
|
|
7
|
+
chosen name is stored in the customer's own config.json.
|
|
8
|
+
|
|
9
|
+
Everything here is chat-only: no tools, any model, or no model at all.
|
|
10
|
+
:class:`NamingFlow` is a plain state machine, so the command-line
|
|
11
|
+
onboarding, the terminal first run and the Telegram first run share the
|
|
12
|
+
same steps and wording.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from typing import Callable, Optional
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger("hyperclaw.naming")
|
|
21
|
+
|
|
22
|
+
# Starter list used when no model is reachable. Written fresh for HyperClaw.
|
|
23
|
+
FALLBACK_NAMES = [
|
|
24
|
+
("Juniper", "It's evergreen and steady, the kind of name that stays useful in every season."),
|
|
25
|
+
("Wren", "Wrens are small, quick and attentive, which is how a good assistant should feel."),
|
|
26
|
+
("Harbor", "A harbor is where things come in and get sorted out safely."),
|
|
27
|
+
("Quill", "It's a nod to all the notes, drafts and messages we'll write together."),
|
|
28
|
+
("Linden", "Linden trees are the shady spot people gather under: calm, dependable company."),
|
|
29
|
+
("Beacon", "It keeps watch and points you to what needs your attention."),
|
|
30
|
+
("Indigo", "A deep, calm color for an assistant that stays unhurried when you're busy."),
|
|
31
|
+
("Compass", "It's there to help you find your way through a crowded day."),
|
|
32
|
+
("Sable", "It's short, distinctive and easy to say in a hurry."),
|
|
33
|
+
("Marlo", "It's friendly and a little uncommon, so it's clearly yours."),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
SUGGEST_WORDS = {"", "suggest", "suggest one", "you pick", "pick one", "surprise me",
|
|
37
|
+
"you choose", "choose", "s", "?"}
|
|
38
|
+
KEEP_WORDS = {"", "y", "yes", "keep", "keep it", "ok", "okay", "sure", "sounds good",
|
|
39
|
+
"love it", "perfect", "k"}
|
|
40
|
+
ANOTHER_WORDS = {"a", "another", "another one", "next", "no", "n", "different",
|
|
41
|
+
"something else", "try again", "again"}
|
|
42
|
+
|
|
43
|
+
MAX_NAME = 32
|
|
44
|
+
_NAME_OK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 .'\-]*$")
|
|
45
|
+
|
|
46
|
+
# ── exact wording (shared by every surface) ─────────────────────────────
|
|
47
|
+
INTRO = ("First, let's give me a name. Type a name you like, or say \"suggest\" "
|
|
48
|
+
"and I'll propose one.")
|
|
49
|
+
PROPOSAL = ("How about {name}? {reason}\n"
|
|
50
|
+
"Say \"yes\" to keep it, \"another\" for a different one, or type a name of your own.")
|
|
51
|
+
CONFIRMED = "{name} it is. That's saved in your config, and you can change it any time with /name."
|
|
52
|
+
FALLBACK_NOTE = ("(I picked that from my starter list because no model is connected yet. "
|
|
53
|
+
"Once one is, /name suggest lets me come up with a name myself.)")
|
|
54
|
+
KEEP_OR_NEW = "Keep '{name}', or choose a new name?"
|
|
55
|
+
KEPT = "Keeping the name {name}."
|
|
56
|
+
# Terminal wording for the same question (no buttons): keep (or Enter) / new.
|
|
57
|
+
KEEP_OR_NEW_PROMPT = "Keep '{name}', or choose a new name? (keep / new, Enter = keep)"
|
|
58
|
+
KEEP_ANSWERS = {"", "keep", "k", "keep it", "yes", "y"}
|
|
59
|
+
NEW_ANSWERS = {"new", "n", "choose new", "choose a new name", "change", "new name"}
|
|
60
|
+
INVALID = ("That name won't work ({why}). Type a name of up to {max} letters, numbers or "
|
|
61
|
+
"spaces, or say \"suggest\".")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_name(text: str) -> tuple:
|
|
65
|
+
"""(clean_name, "") or ("", reason)."""
|
|
66
|
+
name = re.sub(r"\s+", " ", (text or "").strip()).strip("\"'")
|
|
67
|
+
if not name:
|
|
68
|
+
return "", "it's empty"
|
|
69
|
+
if len(name) > MAX_NAME:
|
|
70
|
+
return "", f"it's longer than {MAX_NAME} characters"
|
|
71
|
+
if len(name.split()) > 3:
|
|
72
|
+
return "", "keep it to three words or fewer"
|
|
73
|
+
if not _NAME_OK.match(name) or not re.search(r"[A-Za-z]", name):
|
|
74
|
+
return "", "use letters, numbers, spaces, apostrophes or hyphens"
|
|
75
|
+
return name, ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── proposals ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
PROPOSE_PROMPT = (
|
|
81
|
+
"Suggest ONE short, friendly name for a new personal AI assistant, and give a "
|
|
82
|
+
"one-sentence reason for it. Do not use any of these names: {avoid}. "
|
|
83
|
+
"Reply on a single line exactly in this form:\n"
|
|
84
|
+
"NAME: <name> | REASON: <one sentence>"
|
|
85
|
+
)
|
|
86
|
+
_REPLY = re.compile(r"NAME:\s*(.+?)\s*\|\s*REASON:\s*(.+)", re.I | re.S)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _first_sentence(text: str) -> str:
|
|
90
|
+
t = re.sub(r"\s+", " ", text or "").strip()
|
|
91
|
+
m = re.match(r"(.+?[.!?])(\s|$)", t)
|
|
92
|
+
t = m.group(1) if m else t
|
|
93
|
+
return t[:200]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_model_reply(text: str, avoid: set) -> Optional[tuple]:
|
|
97
|
+
m = _REPLY.search(text or "")
|
|
98
|
+
if not m:
|
|
99
|
+
return None
|
|
100
|
+
name, why = validate_name(m.group(1).strip().strip("*_`"))
|
|
101
|
+
reason = _first_sentence(m.group(2).strip().strip("*_`"))
|
|
102
|
+
if why or not reason or name.lower() in {a.lower() for a in avoid}:
|
|
103
|
+
return None
|
|
104
|
+
if not reason.endswith((".", "!", "?")):
|
|
105
|
+
reason += "."
|
|
106
|
+
return name, reason
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fallback_proposal(avoid: set) -> tuple:
|
|
110
|
+
low = {a.lower() for a in avoid}
|
|
111
|
+
for name, reason in FALLBACK_NAMES:
|
|
112
|
+
if name.lower() not in low:
|
|
113
|
+
return name, reason
|
|
114
|
+
# Every starter name was turned down: cycle deterministically.
|
|
115
|
+
name, reason = FALLBACK_NAMES[len(avoid) % len(FALLBACK_NAMES)]
|
|
116
|
+
return name, reason
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def propose_name(avoid: set, model_call: Optional[Callable[[str], Optional[str]]] = None) -> tuple:
|
|
120
|
+
"""(name, reason, source) with source "model" or "fallback"."""
|
|
121
|
+
if model_call is not None:
|
|
122
|
+
try:
|
|
123
|
+
reply = model_call(PROPOSE_PROMPT.format(
|
|
124
|
+
avoid=", ".join(sorted(avoid)) or "none"))
|
|
125
|
+
parsed = parse_model_reply(reply or "", avoid)
|
|
126
|
+
if parsed:
|
|
127
|
+
return parsed[0], parsed[1], "model"
|
|
128
|
+
except Exception as e: # unreachable, timeout, bad reply: fall back
|
|
129
|
+
log.info(f"name proposal fell back to the starter list: {e}")
|
|
130
|
+
name, reason = fallback_proposal(avoid)
|
|
131
|
+
return name, reason, "fallback"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def default_model_call(timeout: float = 20.0) -> Optional[Callable[[str], Optional[str]]]:
|
|
135
|
+
"""A one-shot, tool-free text call on the configured chat model, or None
|
|
136
|
+
when no model is configured (then the starter list is used)."""
|
|
137
|
+
try:
|
|
138
|
+
from hyperclaw.providers import registry
|
|
139
|
+
cands = registry().resolve("primary", {"chat"})
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
if not cands:
|
|
143
|
+
return None
|
|
144
|
+
prov, model = cands[0]
|
|
145
|
+
|
|
146
|
+
def call(prompt: str) -> Optional[str]:
|
|
147
|
+
from hyperclaw.cost_guard import guard_client, max_output
|
|
148
|
+
if prov.kind == "anthropic":
|
|
149
|
+
import anthropic
|
|
150
|
+
from hyperclaw.api_utils import extract_text
|
|
151
|
+
client = guard_client(anthropic.Anthropic(api_key=prov.api_key, timeout=timeout,
|
|
152
|
+
max_retries=0), getattr(prov, "name", None))
|
|
153
|
+
resp = client.messages.create(model=model,
|
|
154
|
+
max_tokens=max_output("naming", prov.name, model),
|
|
155
|
+
messages=[{"role": "user", "content": prompt}])
|
|
156
|
+
return extract_text(resp)
|
|
157
|
+
import openai
|
|
158
|
+
client = guard_client(openai.OpenAI(api_key=prov.api_key or "unused",
|
|
159
|
+
base_url=prov.base_url or None, timeout=timeout,
|
|
160
|
+
max_retries=0), getattr(prov, "name", None))
|
|
161
|
+
resp = client.chat.completions.create(
|
|
162
|
+
model=model, max_tokens=max_output("naming", prov.name, model),
|
|
163
|
+
messages=[{"role": "user", "content": prompt}])
|
|
164
|
+
return resp.choices[0].message.content if resp.choices else None
|
|
165
|
+
|
|
166
|
+
return call
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── storage ─────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def current_name() -> str:
|
|
172
|
+
from hyperclaw.userconfig import load_config
|
|
173
|
+
return str(load_config().get("ai_name") or "").strip()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_named() -> bool:
|
|
177
|
+
return bool(current_name())
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def save_name(name: str, source: str) -> None:
|
|
181
|
+
"""source: "customer" (typed) or "suggested:model" / "suggested:fallback"."""
|
|
182
|
+
from hyperclaw.userconfig import update_config
|
|
183
|
+
update_config(ai_name=name, ai_name_source=source)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ── the flow ────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
class NamingFlow:
|
|
189
|
+
"""ask -> (typed name | suggestion -> keep / another / typed) -> done."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, model_call: Optional[Callable] = None, save: bool = True):
|
|
192
|
+
self.model_call = model_call
|
|
193
|
+
self.save = save
|
|
194
|
+
self.state = "ask"
|
|
195
|
+
self.proposed: Optional[tuple] = None
|
|
196
|
+
self.offered: list = []
|
|
197
|
+
self.name: str = ""
|
|
198
|
+
self.source: str = ""
|
|
199
|
+
|
|
200
|
+
def start(self) -> str:
|
|
201
|
+
self.state = "ask"
|
|
202
|
+
return INTRO
|
|
203
|
+
|
|
204
|
+
def suggest(self) -> str:
|
|
205
|
+
name, reason, source = propose_name(set(self.offered), self.model_call)
|
|
206
|
+
self.offered.append(name)
|
|
207
|
+
self.proposed = (name, reason, source)
|
|
208
|
+
self.state = "proposed"
|
|
209
|
+
return PROPOSAL.format(name=name, reason=reason)
|
|
210
|
+
|
|
211
|
+
def _finish(self, name: str, source: str) -> str:
|
|
212
|
+
self.name, self.source, self.state = name, source, "done"
|
|
213
|
+
if self.save:
|
|
214
|
+
save_name(name, source)
|
|
215
|
+
msg = CONFIRMED.format(name=name)
|
|
216
|
+
if source == "suggested:fallback":
|
|
217
|
+
msg += "\n" + FALLBACK_NOTE
|
|
218
|
+
return msg
|
|
219
|
+
|
|
220
|
+
def handle(self, text: str) -> str:
|
|
221
|
+
t = (text or "").strip()
|
|
222
|
+
low = t.lower().rstrip(".!")
|
|
223
|
+
if self.state == "ask":
|
|
224
|
+
if low in SUGGEST_WORDS:
|
|
225
|
+
return self.suggest()
|
|
226
|
+
elif self.state == "proposed":
|
|
227
|
+
if low in KEEP_WORDS:
|
|
228
|
+
name, _, source = self.proposed
|
|
229
|
+
return self._finish(name, f"suggested:{source}")
|
|
230
|
+
if low in ANOTHER_WORDS or low in SUGGEST_WORDS:
|
|
231
|
+
return self.suggest()
|
|
232
|
+
elif self.state == "done":
|
|
233
|
+
return CONFIRMED.format(name=self.name)
|
|
234
|
+
name, why = validate_name(t)
|
|
235
|
+
if why:
|
|
236
|
+
return INVALID.format(why=why, max=MAX_NAME)
|
|
237
|
+
return self._finish(name, "customer")
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def done(self) -> bool:
|
|
241
|
+
return self.state == "done"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def run_interactive(ask: Callable[[str], str], say: Callable[[str], None],
|
|
245
|
+
model_call: Optional[Callable] = None, save: bool = True) -> str:
|
|
246
|
+
"""Drive the flow with a prompt function (terminal / onboarding).
|
|
247
|
+
ask(label) returns the customer's line; EOF/empty handling is the
|
|
248
|
+
caller's. Returns the chosen name."""
|
|
249
|
+
flow = NamingFlow(model_call=model_call, save=save)
|
|
250
|
+
say(flow.start())
|
|
251
|
+
label = "Name (Enter = suggest one)"
|
|
252
|
+
for _ in range(50):
|
|
253
|
+
reply = flow.handle(ask(label))
|
|
254
|
+
say(reply)
|
|
255
|
+
if flow.done:
|
|
256
|
+
return flow.name
|
|
257
|
+
label = ("Keep it? yes / another / or type a name" if flow.state == "proposed"
|
|
258
|
+
else "Name (Enter = suggest one)")
|
|
259
|
+
# Pathological input loop: keep whatever was last proposed.
|
|
260
|
+
if flow.proposed:
|
|
261
|
+
say(flow.handle("yes"))
|
|
262
|
+
return flow.name
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run_command(argv: list, model_call: Optional[Callable] = None) -> str:
|
|
266
|
+
"""Non-interactive /name: show, set <name>, suggest (proposal only;
|
|
267
|
+
keep it with `/name <that name>`)."""
|
|
268
|
+
if not argv or argv[0].lower() in ("show",):
|
|
269
|
+
cur = current_name()
|
|
270
|
+
return (f"My name is {cur}." if cur else "I don't have a name yet. "
|
|
271
|
+
"Use /name <name>, or /name suggest.")
|
|
272
|
+
if argv[0].lower() == "suggest":
|
|
273
|
+
name, reason, source = propose_name({current_name()} - {""}, model_call)
|
|
274
|
+
msg = (f"How about {name}? {reason}\nTo keep it, send /name {name} "
|
|
275
|
+
f"(or /name suggest for another).")
|
|
276
|
+
if source == "fallback":
|
|
277
|
+
msg += "\n" + FALLBACK_NOTE
|
|
278
|
+
return msg
|
|
279
|
+
text = " ".join(argv[1:] if argv[0].lower() == "set" else argv)
|
|
280
|
+
name, why = validate_name(text)
|
|
281
|
+
if why:
|
|
282
|
+
return INVALID.format(why=why, max=MAX_NAME)
|
|
283
|
+
save_name(name, "customer")
|
|
284
|
+
return CONFIRMED.format(name=name)
|
hyperclaw/notices.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""User-facing failure notices (standard ruled by Vaughn, 2026-10-10, 20.7).
|
|
2
|
+
|
|
3
|
+
Every notice a user can see when something fails must:
|
|
4
|
+
1. say WHICH action failed ("I couldn't send that message"),
|
|
5
|
+
2. never imply success (no "sent", "done", "delivered", "completed"),
|
|
6
|
+
3. never carry the technical error. That goes to the log via log_failure().
|
|
7
|
+
|
|
8
|
+
All user-visible failure wording lives here so Telegram, the scheduler,
|
|
9
|
+
Solomon, the TUI bridge, the terminal and the HTTP chat say it one way.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# action id -> the verb phrase after "I couldn't ..."
|
|
17
|
+
ACTIONS: dict = {
|
|
18
|
+
"answer": "answer that message",
|
|
19
|
+
"send_message": "send that message",
|
|
20
|
+
"read_file": "read that file",
|
|
21
|
+
"write_file": "save that file",
|
|
22
|
+
"get_answer": "get an answer to that message",
|
|
23
|
+
"morning_brief": "finish the morning brief",
|
|
24
|
+
"heartbeat": "finish the heartbeat check",
|
|
25
|
+
"all_hands": "finish the all-hands report",
|
|
26
|
+
"request": "carry out that request",
|
|
27
|
+
"tool": "run that tool",
|
|
28
|
+
}
|
|
29
|
+
GENERIC_ACTION = "complete that request"
|
|
30
|
+
_RETRY = "Please try again in a moment."
|
|
31
|
+
|
|
32
|
+
# Words a failure notice must never contain (it would read as success).
|
|
33
|
+
SUCCESS_WORDS = re.compile(
|
|
34
|
+
r"\b(sent|done|delivered|completed|succeeded|success|successfully|finished)\b", re.I)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def action_phrase(action: str | None = None, what: str | None = None) -> str:
|
|
38
|
+
"""The phrase naming the failed action. ``what`` is a free-form phrase
|
|
39
|
+
(e.g. "run the calendar tool") and wins over ``action``; an unknown or
|
|
40
|
+
missing action falls back to a generic phrase that still names a request."""
|
|
41
|
+
if what and what.strip():
|
|
42
|
+
return what.strip()
|
|
43
|
+
return ACTIONS.get(action or "", GENERIC_ACTION)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def failure_notice(action: str | None = None, *, what: str | None = None,
|
|
47
|
+
detail: str = "", retry: bool = True) -> str:
|
|
48
|
+
"""'Sorry, I couldn't <action>. <detail> Please try again in a moment.'
|
|
49
|
+
|
|
50
|
+
``detail`` is a plain-language sentence for the user (never exception text).
|
|
51
|
+
"""
|
|
52
|
+
text = f"Sorry, I couldn't {action_phrase(action, what)}."
|
|
53
|
+
if detail:
|
|
54
|
+
text += " " + detail.strip()
|
|
55
|
+
if retry:
|
|
56
|
+
text += " " + _RETRY
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def log_failure(logger: logging.Logger, action: str | None, error=None, *,
|
|
61
|
+
context: str = "", exc_info: bool = False) -> None:
|
|
62
|
+
"""Log the technical error for a failed action (the only place it goes)."""
|
|
63
|
+
logger.error("%s failed%s: %s", action or "action",
|
|
64
|
+
f" ({context})" if context else "", str(error)[:500] if error else "no detail",
|
|
65
|
+
exc_info=exc_info)
|
hyperclaw/oai_stream.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Split OpenAI-compatible streaming deltas into ("thinking" | "text", str).
|
|
2
|
+
|
|
3
|
+
Reasoning models served through vLLM (HyperSpeed V2 runs the qwen3
|
|
4
|
+
reasoning parser) stream their chain of thought in a side field
|
|
5
|
+
(``delta.reasoning_content``; newer vLLM builds call it ``delta.reasoning``)
|
|
6
|
+
and the answer in ``delta.content``. Without a reasoning parser the same
|
|
7
|
+
model writes ``<think>…</think>`` inline in ``content``. Either way the
|
|
8
|
+
reasoning must never be shown or saved as the answer, and the answer's
|
|
9
|
+
leading blank lines (qwen3 starts content with "\\n\\n") are dropped.
|
|
10
|
+
|
|
11
|
+
Used by every OpenAI-schema streaming loop (solomon, the TUI) so they
|
|
12
|
+
classify chunks the same way.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
REASONING_FIELDS = ("reasoning_content", "reasoning")
|
|
16
|
+
OPEN_TAG, CLOSE_TAG = "<think>", "</think>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _field(delta, name):
|
|
20
|
+
if delta is None:
|
|
21
|
+
return None
|
|
22
|
+
if isinstance(delta, dict):
|
|
23
|
+
return delta.get(name)
|
|
24
|
+
val = getattr(delta, name, None)
|
|
25
|
+
if val is None:
|
|
26
|
+
extra = getattr(delta, "model_extra", None) or {}
|
|
27
|
+
val = extra.get(name)
|
|
28
|
+
return val
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _partial_tag_len(buf: str) -> int:
|
|
32
|
+
"""Length of the longest suffix of buf that could start a think tag."""
|
|
33
|
+
for n in range(min(len(buf), len(CLOSE_TAG) - 1), 0, -1):
|
|
34
|
+
tail = buf[-n:]
|
|
35
|
+
if OPEN_TAG.startswith(tail) or CLOSE_TAG.startswith(tail):
|
|
36
|
+
return n
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DeltaSplitter:
|
|
41
|
+
"""Stateful: feed() each streamed delta, flush() once at the end."""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.in_think = False
|
|
45
|
+
self.text_started = False
|
|
46
|
+
self._pending = ""
|
|
47
|
+
self.text_chars = 0
|
|
48
|
+
self.thinking_chars = 0
|
|
49
|
+
|
|
50
|
+
def _emit(self, out, kind, s):
|
|
51
|
+
if not s:
|
|
52
|
+
return
|
|
53
|
+
if kind == "text":
|
|
54
|
+
if not self.text_started:
|
|
55
|
+
s = s.lstrip()
|
|
56
|
+
if not s:
|
|
57
|
+
return
|
|
58
|
+
self.text_started = True
|
|
59
|
+
self.text_chars += len(s)
|
|
60
|
+
else:
|
|
61
|
+
self.thinking_chars += len(s)
|
|
62
|
+
out.append((kind, s))
|
|
63
|
+
|
|
64
|
+
def feed(self, delta) -> list:
|
|
65
|
+
out = []
|
|
66
|
+
for name in REASONING_FIELDS:
|
|
67
|
+
r = _field(delta, name)
|
|
68
|
+
if isinstance(r, str) and r:
|
|
69
|
+
self._emit(out, "thinking", r)
|
|
70
|
+
break
|
|
71
|
+
content = _field(delta, "content")
|
|
72
|
+
if isinstance(content, str) and content:
|
|
73
|
+
self._feed_content(content, out)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
def _feed_content(self, s, out):
|
|
77
|
+
buf = self._pending + s
|
|
78
|
+
self._pending = ""
|
|
79
|
+
while buf:
|
|
80
|
+
if self.in_think:
|
|
81
|
+
i = buf.find(CLOSE_TAG)
|
|
82
|
+
if i >= 0:
|
|
83
|
+
self._emit(out, "thinking", buf[:i])
|
|
84
|
+
buf = buf[i + len(CLOSE_TAG):]
|
|
85
|
+
self.in_think = False
|
|
86
|
+
continue
|
|
87
|
+
else:
|
|
88
|
+
o, c = buf.find(OPEN_TAG), buf.find(CLOSE_TAG)
|
|
89
|
+
hits = [(i, t) for i, t in ((o, OPEN_TAG), (c, CLOSE_TAG)) if i >= 0]
|
|
90
|
+
if hits:
|
|
91
|
+
i, tag = min(hits)
|
|
92
|
+
self._emit(out, "text", buf[:i])
|
|
93
|
+
buf = buf[i + len(tag):]
|
|
94
|
+
self.in_think = tag == OPEN_TAG # a stray </think> is dropped
|
|
95
|
+
continue
|
|
96
|
+
keep = _partial_tag_len(buf)
|
|
97
|
+
self._emit(out, "thinking" if self.in_think else "text", buf[:len(buf) - keep])
|
|
98
|
+
self._pending = buf[len(buf) - keep:]
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
def flush(self) -> list:
|
|
102
|
+
out = []
|
|
103
|
+
if self._pending:
|
|
104
|
+
self._emit(out, "thinking" if self.in_think else "text", self._pending)
|
|
105
|
+
self._pending = ""
|
|
106
|
+
return out
|