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/team/model.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The model a specialist talks to.
|
|
3
|
+
|
|
4
|
+
A ``ModelHandle`` gives the specialist loop one call shape (Anthropic
|
|
5
|
+
Messages: system, messages, tools -> content blocks + stop_reason) whatever
|
|
6
|
+
the provider is. Every call:
|
|
7
|
+
|
|
8
|
+
- goes through a cost-guarded client (``cost_guard.guard_client``), so it is
|
|
9
|
+
reserved against the per-task and daily caps BEFORE it is made and settled
|
|
10
|
+
after; at a cap it raises ``CostCapReached`` and nothing is called;
|
|
11
|
+
- sets an explicit max output (``max_tokens``; HC-037);
|
|
12
|
+
- comes from a provider whose tool use is declared AND qualified (signed
|
|
13
|
+
record). Without one the specialist does not run (``NoToolModel``): the
|
|
14
|
+
failure is reported, never papered over with a chat-only model.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import uuid
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from types import SimpleNamespace
|
|
22
|
+
from typing import Any, Callable, List, Optional
|
|
23
|
+
|
|
24
|
+
from hyperclaw.team.roster import Specialist
|
|
25
|
+
|
|
26
|
+
MAX_OUTPUT_PATH = "specialist" # cost_guard.DEFAULT_MAX_OUTPUT_PER_PATH (!33)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoToolModel(RuntimeError):
|
|
30
|
+
"""No provider with qualified tool use is configured for specialists."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
super().__init__("no model with qualified tool use is configured, so the specialist "
|
|
34
|
+
"could not run")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ModelHandle:
|
|
39
|
+
client: Any # cost-guarded, Anthropic Messages shape (.messages.create)
|
|
40
|
+
model: str
|
|
41
|
+
provider: str = ""
|
|
42
|
+
|
|
43
|
+
def create(self, *, system: str, messages: list, tools: list) -> Any:
|
|
44
|
+
from hyperclaw.cost_guard import max_output
|
|
45
|
+
extra = {"tools": tools} if tools else {}
|
|
46
|
+
return self.client.messages.create(
|
|
47
|
+
model=self.model, max_tokens=max_output(MAX_OUTPUT_PATH, self.provider, self.model),
|
|
48
|
+
system=system, messages=messages, **extra)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ───────────────────────── OpenAI-compatible providers ─────────────────────────
|
|
52
|
+
|
|
53
|
+
class _OpenAIMessages:
|
|
54
|
+
"""Anthropic-shaped .create() over a cost-guarded chat.completions client."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, client, provider_name: str, base_url: str):
|
|
57
|
+
self._client = client
|
|
58
|
+
self._provider = provider_name
|
|
59
|
+
self._base = base_url
|
|
60
|
+
|
|
61
|
+
def create(self, *, model: str, max_tokens: int, system: str, messages: list,
|
|
62
|
+
tools: Optional[list] = None):
|
|
63
|
+
from hyperclaw.truth_guard import history_to_openai
|
|
64
|
+
from hyperclaw.xai import clean_request
|
|
65
|
+
oai = [{"role": "system", "content": system}] + history_to_openai(messages,
|
|
66
|
+
native_tools=True)
|
|
67
|
+
kw = dict(model=model, messages=oai, max_tokens=int(max_tokens), stream=False)
|
|
68
|
+
if tools:
|
|
69
|
+
kw["tools"] = [{"type": "function", "function": {
|
|
70
|
+
"name": t["name"], "description": t.get("description", ""),
|
|
71
|
+
"parameters": t.get("input_schema", {})}} for t in tools]
|
|
72
|
+
kw["tool_choice"] = "auto"
|
|
73
|
+
kw = clean_request(self._provider, self._base, model, kw)
|
|
74
|
+
res = self._client.chat.completions.create(**kw)
|
|
75
|
+
ch = (getattr(res, "choices", None) or [None])[0]
|
|
76
|
+
msg = getattr(ch, "message", None)
|
|
77
|
+
blocks: List[Any] = []
|
|
78
|
+
text = getattr(msg, "content", None) or ""
|
|
79
|
+
if text:
|
|
80
|
+
blocks.append(SimpleNamespace(type="text", text=text))
|
|
81
|
+
for tc in getattr(msg, "tool_calls", None) or []:
|
|
82
|
+
fn = getattr(tc, "function", None)
|
|
83
|
+
raw = getattr(fn, "arguments", "") or "{}"
|
|
84
|
+
try:
|
|
85
|
+
args = json.loads(raw)
|
|
86
|
+
except ValueError:
|
|
87
|
+
args = raw # the registry answers invalid_args, never {}
|
|
88
|
+
blocks.append(SimpleNamespace(type="tool_use", id=getattr(tc, "id", "") or
|
|
89
|
+
f"call_{uuid.uuid4().hex[:12]}",
|
|
90
|
+
name=getattr(fn, "name", ""), input=args))
|
|
91
|
+
fr = getattr(ch, "finish_reason", None)
|
|
92
|
+
stop = {"tool_calls": "tool_use", "length": "max_tokens"}.get(fr, "end_turn")
|
|
93
|
+
return SimpleNamespace(content=blocks, stop_reason=stop)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _OpenAIShim:
|
|
97
|
+
def __init__(self, client, provider_name: str, base_url: str):
|
|
98
|
+
self.messages = _OpenAIMessages(client, provider_name, base_url)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ───────────────────────── resolution ─────────────────────────
|
|
102
|
+
|
|
103
|
+
def _candidates(spec: Specialist) -> list:
|
|
104
|
+
from hyperclaw.providers import registry
|
|
105
|
+
reg = registry()
|
|
106
|
+
out = []
|
|
107
|
+
for slot in (spec.tier, "tools", "primary"):
|
|
108
|
+
for c in reg.resolve(slot, {"chat", "tool_use"}):
|
|
109
|
+
if c not in out:
|
|
110
|
+
out.append(c)
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def default_handle(spec: Specialist) -> ModelHandle:
|
|
115
|
+
"""A cost-guarded handle on a qualified tool-use model, preferring the
|
|
116
|
+
specialist's tier. Raises NoToolModel when none is configured."""
|
|
117
|
+
from hyperclaw.cost_guard import guard_client
|
|
118
|
+
cands = _candidates(spec)
|
|
119
|
+
if not cands:
|
|
120
|
+
raise NoToolModel()
|
|
121
|
+
prov, model = cands[0]
|
|
122
|
+
if prov.kind == "anthropic":
|
|
123
|
+
import anthropic
|
|
124
|
+
client = guard_client(anthropic.Anthropic(api_key=prov.api_key,
|
|
125
|
+
base_url=prov.base_url or None,
|
|
126
|
+
timeout=60.0, max_retries=0), prov.name)
|
|
127
|
+
return ModelHandle(client, model, prov.name)
|
|
128
|
+
import openai
|
|
129
|
+
raw = guard_client(openai.OpenAI(api_key=prov.api_key or "unused",
|
|
130
|
+
base_url=prov.base_url or None, timeout=60.0,
|
|
131
|
+
max_retries=0), prov.name)
|
|
132
|
+
return ModelHandle(_OpenAIShim(raw, prov.name, prov.base_url or ""), model, prov.name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
HandleFactory = Callable[[Specialist], ModelHandle]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The morning brief's "your day" sections (HC-038), at each person's chosen
|
|
3
|
+
time (``brief.time``), extending the existing daily brief: the same
|
|
4
|
+
scheduled job (one background service) and the same Telegram delivery (one
|
|
5
|
+
Telegram connection, link previews off).
|
|
6
|
+
|
|
7
|
+
Four sections, each from whichever connector is present:
|
|
8
|
+
|
|
9
|
+
Today's calendar CalendarSource
|
|
10
|
+
Email that needs you MailSource
|
|
11
|
+
Slack mentions and DMs SlackSource
|
|
12
|
+
MRs waiting on you ReposSource
|
|
13
|
+
|
|
14
|
+
A missing connector is skipped honestly ("not connected"), a failed read is
|
|
15
|
+
named ("could not read ..."), and an empty one says "nothing". Nothing is
|
|
16
|
+
written by a model here: every line is taken from what the source returned,
|
|
17
|
+
so the brief never claims anything that is not on this install.
|
|
18
|
+
|
|
19
|
+
Integration points (marked ``INTEGRATION``): the Gmail / Calendar / Slack
|
|
20
|
+
readers may be replaced by the connector lane's readers; each source is a
|
|
21
|
+
small class with ``connected()`` and ``read(now)``, and tests use fakes.
|
|
22
|
+
Everything read is connector content (``hyperclaw.taint``): it is delivered
|
|
23
|
+
to the owner and never saved to memory, learning or training data.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from datetime import datetime, timedelta, timezone
|
|
30
|
+
from typing import List, Optional, Sequence
|
|
31
|
+
|
|
32
|
+
log = logging.getLogger("hyperclaw.team.morning")
|
|
33
|
+
|
|
34
|
+
MAX_LINES = 8
|
|
35
|
+
LINE_CHARS = 140
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SourceUnavailable(Exception):
|
|
39
|
+
"""A connected source that could not be read (the reason is shown)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Section:
|
|
44
|
+
title: str
|
|
45
|
+
lines: List[str]
|
|
46
|
+
state: str = "ok" # ok | not_connected | failed
|
|
47
|
+
|
|
48
|
+
def render(self) -> str:
|
|
49
|
+
if self.state == "not_connected":
|
|
50
|
+
return f"{self.title}: not connected."
|
|
51
|
+
if self.state == "failed":
|
|
52
|
+
return f"{self.title}: could not read it ({self.lines[0] if self.lines else 'error'})."
|
|
53
|
+
if not self.lines:
|
|
54
|
+
return f"{self.title}: nothing."
|
|
55
|
+
shown = self.lines[:MAX_LINES]
|
|
56
|
+
more = len(self.lines) - len(shown)
|
|
57
|
+
body = "\n".join(f"- {_clip(l)}" for l in shown)
|
|
58
|
+
return f"{self.title}:\n{body}" + (f"\n- and {more} more" if more > 0 else "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _clip(s: str) -> str:
|
|
62
|
+
"""One line, no web addresses (link previews and taps stay off)."""
|
|
63
|
+
from hyperclaw.team.watchers import _SPACE, _URL
|
|
64
|
+
t = _SPACE.sub(" ", _URL.sub("[link removed]", str(s or ""))).strip()
|
|
65
|
+
return t if len(t) <= LINE_CHARS else t[:LINE_CHARS - 1].rstrip() + "…"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Source:
|
|
69
|
+
title = ""
|
|
70
|
+
|
|
71
|
+
def connected(self) -> bool:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
def read(self, now: datetime) -> List[str]:
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ───────────────────────── default sources ─────────────────────────
|
|
79
|
+
|
|
80
|
+
def _google_connected() -> bool:
|
|
81
|
+
try:
|
|
82
|
+
from hyperclaw.api_utils import gmail_configured
|
|
83
|
+
return gmail_configured()
|
|
84
|
+
except Exception:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CalendarSource(Source):
|
|
89
|
+
"""INTEGRATION: Google Calendar via integrations_layer (the connector
|
|
90
|
+
lane's reader replaces ``_events``)."""
|
|
91
|
+
title = "Today's calendar"
|
|
92
|
+
|
|
93
|
+
def connected(self) -> bool:
|
|
94
|
+
return _google_connected()
|
|
95
|
+
|
|
96
|
+
def _events(self) -> list:
|
|
97
|
+
from hyperclaw import integrations_layer as il
|
|
98
|
+
return il.calendar_get_events(days_ahead=1)
|
|
99
|
+
|
|
100
|
+
def read(self, now: datetime) -> List[str]:
|
|
101
|
+
events = self._events()
|
|
102
|
+
if events and isinstance(events[0], dict) and events[0].get("error"):
|
|
103
|
+
raise SourceUnavailable("Google Calendar refused the request")
|
|
104
|
+
day = now.date().isoformat()
|
|
105
|
+
out = []
|
|
106
|
+
for e in events:
|
|
107
|
+
start = str(e.get("start") or "")
|
|
108
|
+
if start[:10] != day:
|
|
109
|
+
continue
|
|
110
|
+
when = start[11:16] if len(start) > 10 else "all day"
|
|
111
|
+
out.append(f"{when} {e.get('summary') or 'Untitled'}")
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MailSource(Source):
|
|
116
|
+
"""INTEGRATION: Gmail via integrations_layer: unread mail in the inbox
|
|
117
|
+
from the last day that Gmail marks important."""
|
|
118
|
+
title = "Email that needs you"
|
|
119
|
+
QUERY = "in:inbox is:unread is:important newer_than:1d"
|
|
120
|
+
|
|
121
|
+
def connected(self) -> bool:
|
|
122
|
+
return _google_connected()
|
|
123
|
+
|
|
124
|
+
def _search(self) -> list:
|
|
125
|
+
from hyperclaw import integrations_layer as il
|
|
126
|
+
return il.gmail_search(self.QUERY, max_results=15)
|
|
127
|
+
|
|
128
|
+
def read(self, now: datetime) -> List[str]:
|
|
129
|
+
mails = self._search()
|
|
130
|
+
if mails and isinstance(mails[0], dict) and mails[0].get("error"):
|
|
131
|
+
raise SourceUnavailable("Gmail refused the request")
|
|
132
|
+
return [f"{m.get('from') or 'someone'}: {m.get('subject') or '(no subject)'}"
|
|
133
|
+
for m in mails if isinstance(m, dict) and not m.get("is_sent")]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SlackSource(Source):
|
|
137
|
+
"""INTEGRATION: the connected digest's read-only Slack collector (the
|
|
138
|
+
owner's user token): mentions and DMs since yesterday morning."""
|
|
139
|
+
title = "Slack mentions and DMs"
|
|
140
|
+
|
|
141
|
+
def connected(self) -> bool:
|
|
142
|
+
try:
|
|
143
|
+
from hyperclaw.digest import state
|
|
144
|
+
return "slack" in state.connections()
|
|
145
|
+
except Exception:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def _collect(self, since: datetime):
|
|
149
|
+
from hyperclaw.digest import service, state
|
|
150
|
+
meta = state.connections().get("slack") or {}
|
|
151
|
+
token = service._read_token("slack", meta)
|
|
152
|
+
if not token:
|
|
153
|
+
raise SourceUnavailable("the Slack token is missing")
|
|
154
|
+
col = service.make_collector("slack", token)
|
|
155
|
+
try:
|
|
156
|
+
col.verify()
|
|
157
|
+
return col.collect(since)
|
|
158
|
+
finally:
|
|
159
|
+
col.close()
|
|
160
|
+
|
|
161
|
+
def read(self, now: datetime) -> List[str]:
|
|
162
|
+
from hyperclaw.digest.errors import DigestError
|
|
163
|
+
try:
|
|
164
|
+
res = self._collect(now - timedelta(hours=24))
|
|
165
|
+
except DigestError as e:
|
|
166
|
+
raise SourceUnavailable(e.text)
|
|
167
|
+
if not res.ok:
|
|
168
|
+
raise SourceUnavailable("Slack could not be read")
|
|
169
|
+
out = []
|
|
170
|
+
for i in res.items:
|
|
171
|
+
if not isinstance(i, dict):
|
|
172
|
+
continue
|
|
173
|
+
where = "DM" if i.get("kind") in ("dm", "group_dm") else i.get("where") or "Slack"
|
|
174
|
+
out.append(f"{i.get('from') or 'someone'} ({where}): {i.get('text') or ''}")
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ReposSource(Source):
|
|
179
|
+
title = "MRs waiting on you"
|
|
180
|
+
|
|
181
|
+
def connected(self) -> bool:
|
|
182
|
+
try:
|
|
183
|
+
from hyperclaw.digest import state
|
|
184
|
+
return "gitlab" in state.connections()
|
|
185
|
+
except Exception:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def read(self, now: datetime) -> List[str]:
|
|
189
|
+
from hyperclaw.team import repos
|
|
190
|
+
items, problem = repos.waiting(now)
|
|
191
|
+
if problem:
|
|
192
|
+
raise SourceUnavailable(problem.rstrip("."))
|
|
193
|
+
out = []
|
|
194
|
+
for i in items:
|
|
195
|
+
if i.get("kind") == "todo":
|
|
196
|
+
out.append(f"to-do: {i.get('action') or ''} {i.get('title') or ''}".strip())
|
|
197
|
+
else:
|
|
198
|
+
role = "review" if i.get("kind") == "mr_reviewing" else "assigned"
|
|
199
|
+
out.append(f"{i.get('ref') or ''} {i.get('title') or ''} ({role})")
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def default_sources() -> List[Source]:
|
|
204
|
+
return [CalendarSource(), MailSource(), SlackSource(), ReposSource()]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ───────────────────────── compose ─────────────────────────
|
|
208
|
+
|
|
209
|
+
def sections(now: Optional[datetime] = None,
|
|
210
|
+
sources: Optional[Sequence[Source]] = None) -> List[Section]:
|
|
211
|
+
now = now or datetime.now(timezone.utc)
|
|
212
|
+
out = []
|
|
213
|
+
for src in (default_sources() if sources is None else sources):
|
|
214
|
+
try:
|
|
215
|
+
if not src.connected():
|
|
216
|
+
out.append(Section(src.title, [], "not_connected"))
|
|
217
|
+
continue
|
|
218
|
+
out.append(Section(src.title, list(src.read(now))))
|
|
219
|
+
except SourceUnavailable as e:
|
|
220
|
+
out.append(Section(src.title, [str(e)], "failed"))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
log.warning("morning: %s not read (%s)", src.title, type(e).__name__, exc_info=True)
|
|
223
|
+
out.append(Section(src.title, ["an internal error"], "failed"))
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def render(now: Optional[datetime] = None,
|
|
228
|
+
sources: Optional[Sequence[Source]] = None) -> str:
|
|
229
|
+
return "\n\n".join(s.render() for s in sections(now, sources))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def enabled() -> bool:
|
|
233
|
+
"""On unless the person turned it off (config brief.morning = false)."""
|
|
234
|
+
try:
|
|
235
|
+
from hyperclaw.brief import store
|
|
236
|
+
return store.config().get("morning", True) is not False
|
|
237
|
+
except Exception:
|
|
238
|
+
return True
|
hyperclaw/team/repos.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Merge requests and to-dos waiting on the person (the Repos specialist's one
|
|
3
|
+
tool, and the morning brief's "MRs waiting on you" section).
|
|
4
|
+
|
|
5
|
+
Read-only, through the connected digest's GitLab connection (read-only token,
|
|
6
|
+
verified scopes; ``hyperclaw.digest.gitlab``). Everything returned is GitLab
|
|
7
|
+
content: marked as connector content (``hyperclaw.taint``).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from typing import Callable, Optional
|
|
14
|
+
|
|
15
|
+
NOT_CONNECTED = "GitLab is not connected (hyperclaw digest connect gitlab)."
|
|
16
|
+
WINDOW = timedelta(days=14)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _collect(now: datetime, *, collector_factory: Optional[Callable] = None):
|
|
20
|
+
from hyperclaw.digest import service, state
|
|
21
|
+
from hyperclaw.digest.errors import DigestError
|
|
22
|
+
meta = state.connections().get("gitlab")
|
|
23
|
+
if not meta:
|
|
24
|
+
return None, NOT_CONNECTED
|
|
25
|
+
col = None
|
|
26
|
+
try:
|
|
27
|
+
if collector_factory is not None:
|
|
28
|
+
col = collector_factory()
|
|
29
|
+
else:
|
|
30
|
+
token = service._read_token("gitlab", meta)
|
|
31
|
+
if not token:
|
|
32
|
+
return None, NOT_CONNECTED
|
|
33
|
+
col = service.make_collector("gitlab", token, gitlab_url=meta.get("url", ""))
|
|
34
|
+
col.verify()
|
|
35
|
+
return col.collect(now - WINDOW), ""
|
|
36
|
+
except DigestError as e:
|
|
37
|
+
return None, f"Could not read GitLab: {e.text}"
|
|
38
|
+
finally:
|
|
39
|
+
if col is not None:
|
|
40
|
+
try:
|
|
41
|
+
col.close()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def waiting(now: Optional[datetime] = None, *, collector_factory: Optional[Callable] = None):
|
|
47
|
+
"""(items, problem). items: MRs assigned to / awaiting review from the
|
|
48
|
+
person, and their pending to-dos. problem: '' or why nothing was read."""
|
|
49
|
+
now = now or datetime.now(timezone.utc)
|
|
50
|
+
res, problem = _collect(now, collector_factory=collector_factory)
|
|
51
|
+
if res is None:
|
|
52
|
+
return [], problem
|
|
53
|
+
if not res.ok:
|
|
54
|
+
return [], "Could not read GitLab."
|
|
55
|
+
keep = ("mr_assigned", "mr_reviewing", "todo")
|
|
56
|
+
return [i for i in res.items if isinstance(i, dict) and i.get("kind") in keep], ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def tool_review_queue(args: dict) -> str:
|
|
60
|
+
"""The gitlab_review_queue tool: JSON list, or a not-connected / failure line."""
|
|
61
|
+
items, problem = waiting()
|
|
62
|
+
if problem:
|
|
63
|
+
return problem
|
|
64
|
+
out = [{k: i.get(k) for k in ("kind", "ref", "title", "author", "from", "action", "updated",
|
|
65
|
+
"when", "link") if i.get(k)} for i in items]
|
|
66
|
+
return json.dumps({"waiting_on_you": out, "count": len(out)})
|