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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .bandit import (
|
|
2
|
+
HyperRouter,
|
|
3
|
+
ucb1_score,
|
|
4
|
+
select_agent,
|
|
5
|
+
select_model,
|
|
6
|
+
update_scores,
|
|
7
|
+
MODEL_COSTS,
|
|
8
|
+
MODEL_LATENCY_MS,
|
|
9
|
+
CHATJIMMY_TASK_TYPES,
|
|
10
|
+
)
|
|
11
|
+
from .fast_loop import FastLoop
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"HyperRouter",
|
|
15
|
+
"ucb1_score",
|
|
16
|
+
"select_agent",
|
|
17
|
+
"select_model",
|
|
18
|
+
"update_scores",
|
|
19
|
+
"MODEL_COSTS",
|
|
20
|
+
"MODEL_LATENCY_MS",
|
|
21
|
+
"CHATJIMMY_TASK_TYPES",
|
|
22
|
+
"FastLoop",
|
|
23
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperRouter UCB1 Bandit — multi-armed bandit model and agent routing.
|
|
3
|
+
Uses Upper Confidence Bound 1 (UCB1) algorithm for exploration/exploitation.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import math
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from core.hyperstate.schema import AgentScore, ModelScore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Cost and latency tables ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
MODEL_COSTS: dict[str, float] = {
|
|
18
|
+
"chatjimmy": 0.000001,
|
|
19
|
+
"claude-opus-5": 0.005,
|
|
20
|
+
"claude-sonnet-5": 0.002,
|
|
21
|
+
"claude-code": 0.003,
|
|
22
|
+
"nim-local": 0.0,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
MODEL_LATENCY_MS: dict[str, float] = {
|
|
26
|
+
"chatjimmy": 50.0,
|
|
27
|
+
"claude-opus-5": 4000.0,
|
|
28
|
+
"claude-sonnet-5": 2000.0,
|
|
29
|
+
"claude-code": 3000.0,
|
|
30
|
+
"nim-local": 500.0,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Tasks ChatJimmy handles well (fast, cheap, low-stakes)
|
|
34
|
+
CHATJIMMY_TASK_TYPES: frozenset[str] = frozenset([
|
|
35
|
+
"routing", "classification", "quick_lookup",
|
|
36
|
+
"summarization", "draft", "triage", "status_check",
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Core UCB1 functions ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def ucb1_score(
|
|
43
|
+
successes: int,
|
|
44
|
+
attempts: int,
|
|
45
|
+
total_attempts: int,
|
|
46
|
+
c: float = 2.0,
|
|
47
|
+
) -> float:
|
|
48
|
+
"""
|
|
49
|
+
Compute UCB1 score for a single arm.
|
|
50
|
+
|
|
51
|
+
Returns float('inf') if attempts == 0 (unexplored arm always gets tried first).
|
|
52
|
+
Formula: mean_reward + c * sqrt(ln(total_attempts) / attempts)
|
|
53
|
+
"""
|
|
54
|
+
if attempts == 0:
|
|
55
|
+
return float("inf")
|
|
56
|
+
mean_reward = successes / attempts
|
|
57
|
+
exploration = c * math.sqrt(math.log(max(total_attempts, 1)) / attempts)
|
|
58
|
+
return mean_reward + exploration
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def select_agent(
|
|
62
|
+
agents: dict[str, AgentScore],
|
|
63
|
+
task_type: str,
|
|
64
|
+
total_attempts: int,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Select the best agent for a task_type using UCB1.
|
|
68
|
+
Prefers agents whose task_type matches, falls back to all agents.
|
|
69
|
+
Returns the agent_id with the highest UCB1 score.
|
|
70
|
+
"""
|
|
71
|
+
if not agents:
|
|
72
|
+
return "default_agent"
|
|
73
|
+
|
|
74
|
+
# Filter to agents matching task_type if possible
|
|
75
|
+
matching = {
|
|
76
|
+
aid: score for aid, score in agents.items()
|
|
77
|
+
if score.task_type == task_type
|
|
78
|
+
}
|
|
79
|
+
pool = matching if matching else agents
|
|
80
|
+
|
|
81
|
+
best_id = max(
|
|
82
|
+
pool,
|
|
83
|
+
key=lambda aid: ucb1_score(
|
|
84
|
+
pool[aid].successes,
|
|
85
|
+
pool[aid].attempts,
|
|
86
|
+
total_attempts,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
return best_id
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def select_model(
|
|
93
|
+
model_scores: dict[str, dict[str, ModelScore]],
|
|
94
|
+
task_type: str,
|
|
95
|
+
total_attempts: int,
|
|
96
|
+
cost_budget_usd: Optional[float] = None,
|
|
97
|
+
latency_budget_ms: Optional[float] = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Select the best model for a task_type using UCB1, with optional budget filters.
|
|
101
|
+
|
|
102
|
+
ChatJimmy is the default for CHATJIMMY_TASK_TYPES if it passes budget checks.
|
|
103
|
+
Budget filters are applied before scoring — models exceeding either budget are excluded.
|
|
104
|
+
Falls back to 'claude-opus-5' if no scored models available.
|
|
105
|
+
"""
|
|
106
|
+
# ChatJimmy fast-path for cheap/quick tasks
|
|
107
|
+
if task_type in CHATJIMMY_TASK_TYPES:
|
|
108
|
+
cj_cost = MODEL_COSTS.get("chatjimmy", 0.0)
|
|
109
|
+
cj_latency = MODEL_LATENCY_MS.get("chatjimmy", 50.0)
|
|
110
|
+
cost_ok = cost_budget_usd is None or cj_cost <= cost_budget_usd
|
|
111
|
+
latency_ok = latency_budget_ms is None or cj_latency <= latency_budget_ms
|
|
112
|
+
if cost_ok and latency_ok:
|
|
113
|
+
return "chatjimmy"
|
|
114
|
+
|
|
115
|
+
# Build candidate pool with budget filtering
|
|
116
|
+
candidates: dict[str, ModelScore] = {}
|
|
117
|
+
for model_id, task_map in model_scores.items():
|
|
118
|
+
score = task_map.get(task_type, ModelScore())
|
|
119
|
+
|
|
120
|
+
cost = MODEL_COSTS.get(model_id, 0.001)
|
|
121
|
+
latency = MODEL_LATENCY_MS.get(model_id, 2000.0)
|
|
122
|
+
|
|
123
|
+
if cost_budget_usd is not None and cost > cost_budget_usd:
|
|
124
|
+
continue
|
|
125
|
+
if latency_budget_ms is not None and latency > latency_budget_ms:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
candidates[model_id] = score
|
|
129
|
+
|
|
130
|
+
if not candidates:
|
|
131
|
+
# Fallback: try any model within budget ignoring task_type filter
|
|
132
|
+
for model_id in MODEL_COSTS:
|
|
133
|
+
cost = MODEL_COSTS.get(model_id, 0.001)
|
|
134
|
+
latency = MODEL_LATENCY_MS.get(model_id, 2000.0)
|
|
135
|
+
if cost_budget_usd is not None and cost > cost_budget_usd:
|
|
136
|
+
continue
|
|
137
|
+
if latency_budget_ms is not None and latency > latency_budget_ms:
|
|
138
|
+
continue
|
|
139
|
+
candidates[model_id] = ModelScore()
|
|
140
|
+
|
|
141
|
+
if not candidates:
|
|
142
|
+
return "claude-opus-5"
|
|
143
|
+
|
|
144
|
+
best_model = max(
|
|
145
|
+
candidates,
|
|
146
|
+
key=lambda mid: ucb1_score(
|
|
147
|
+
candidates[mid].successes,
|
|
148
|
+
candidates[mid].attempts,
|
|
149
|
+
total_attempts,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
return best_model
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def update_scores(
|
|
156
|
+
scores: dict[str, dict[str, ModelScore]],
|
|
157
|
+
model_id: str,
|
|
158
|
+
task_type: str,
|
|
159
|
+
success: bool,
|
|
160
|
+
) -> dict[str, dict[str, ModelScore]]:
|
|
161
|
+
"""
|
|
162
|
+
Immutably update model scores. Returns a new dict — original is unchanged.
|
|
163
|
+
"""
|
|
164
|
+
new_scores = copy.deepcopy(scores)
|
|
165
|
+
if model_id not in new_scores:
|
|
166
|
+
new_scores[model_id] = {}
|
|
167
|
+
if task_type not in new_scores[model_id]:
|
|
168
|
+
new_scores[model_id][task_type] = ModelScore()
|
|
169
|
+
|
|
170
|
+
entry = new_scores[model_id][task_type]
|
|
171
|
+
entry.attempts += 1
|
|
172
|
+
if success:
|
|
173
|
+
entry.successes += 1
|
|
174
|
+
|
|
175
|
+
return new_scores
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── HyperRouter dataclass ──────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class HyperRouter:
|
|
182
|
+
"""
|
|
183
|
+
Stateful router that holds live UCB1 scores and routes tasks to
|
|
184
|
+
(agent_id, model_id) pairs.
|
|
185
|
+
"""
|
|
186
|
+
agent_scores: dict[str, AgentScore] = field(default_factory=dict)
|
|
187
|
+
model_scores: dict[str, dict[str, ModelScore]] = field(default_factory=dict)
|
|
188
|
+
total_attempts: int = 0
|
|
189
|
+
|
|
190
|
+
def route(
|
|
191
|
+
self,
|
|
192
|
+
task_type: str,
|
|
193
|
+
cost_budget: Optional[float] = None,
|
|
194
|
+
latency_budget: Optional[float] = None,
|
|
195
|
+
) -> tuple[str, str]:
|
|
196
|
+
"""
|
|
197
|
+
Route a task to the best (agent_id, model_id) pair.
|
|
198
|
+
Uses UCB1 for both selections.
|
|
199
|
+
"""
|
|
200
|
+
agent_id = select_agent(self.agent_scores, task_type, self.total_attempts)
|
|
201
|
+
model_id = select_model(
|
|
202
|
+
self.model_scores,
|
|
203
|
+
task_type,
|
|
204
|
+
self.total_attempts,
|
|
205
|
+
cost_budget_usd=cost_budget,
|
|
206
|
+
latency_budget_ms=latency_budget,
|
|
207
|
+
)
|
|
208
|
+
return agent_id, model_id
|
|
209
|
+
|
|
210
|
+
def record(self, agent_id: str, model_id: str, task_type: str, success: bool) -> None:
|
|
211
|
+
"""Record the outcome of a routing decision and update scores."""
|
|
212
|
+
self.total_attempts += 1
|
|
213
|
+
self.model_scores = update_scores(self.model_scores, model_id, task_type, success)
|
|
214
|
+
if agent_id not in self.agent_scores:
|
|
215
|
+
self.agent_scores[agent_id] = AgentScore(task_type=task_type)
|
|
216
|
+
self.agent_scores[agent_id].attempts += 1
|
|
217
|
+
if success:
|
|
218
|
+
self.agent_scores[agent_id].successes += 1
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastLoop — high-frequency routing loop. Routes tasks in <100ms using cached UCB1 scores.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from core.hyperstate.schema import ExperimentEntry, HyperState
|
|
10
|
+
from .bandit import HyperRouter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FastLoop:
|
|
14
|
+
"""
|
|
15
|
+
Fast routing path. Reads live UCB1 scores from HyperState.model_scores
|
|
16
|
+
and routes to the optimal (agent_id, model_id) pair in microseconds.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._router = HyperRouter()
|
|
21
|
+
|
|
22
|
+
async def route(
|
|
23
|
+
self,
|
|
24
|
+
state: HyperState,
|
|
25
|
+
task_type: str,
|
|
26
|
+
cost_budget: Optional[float] = None,
|
|
27
|
+
latency_budget: Optional[float] = None,
|
|
28
|
+
) -> tuple[str, str]:
|
|
29
|
+
"""
|
|
30
|
+
Route a task using live UCB1 scores from the HyperState.
|
|
31
|
+
|
|
32
|
+
Syncs the router's model_scores from state, routes the task,
|
|
33
|
+
then appends a routing entry to state.experiment_log.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
(agent_id, model_id)
|
|
37
|
+
"""
|
|
38
|
+
# Sync router from state's live scores
|
|
39
|
+
self._router.agent_scores = dict(state.agent_scores)
|
|
40
|
+
self._router.model_scores = {
|
|
41
|
+
model_id: dict(task_map)
|
|
42
|
+
for model_id, task_map in state.model_scores.items()
|
|
43
|
+
}
|
|
44
|
+
self._router.total_attempts = sum(
|
|
45
|
+
score.attempts for score in state.agent_scores.values()
|
|
46
|
+
) or 1
|
|
47
|
+
|
|
48
|
+
agent_id, model_id = self._router.route(
|
|
49
|
+
task_type=task_type,
|
|
50
|
+
cost_budget=cost_budget,
|
|
51
|
+
latency_budget=latency_budget,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Log routing decision to state experiment log
|
|
55
|
+
routing_entry = ExperimentEntry(
|
|
56
|
+
method=f"fast_loop_route:{task_type}",
|
|
57
|
+
model_used=model_id,
|
|
58
|
+
result=f"routed to agent={agent_id} model={model_id}",
|
|
59
|
+
certified=False,
|
|
60
|
+
test_trace="",
|
|
61
|
+
cost_usd=0.0,
|
|
62
|
+
latency_ms=0.0,
|
|
63
|
+
timestamp=datetime.now(timezone.utc),
|
|
64
|
+
)
|
|
65
|
+
state.experiment_log.append(routing_entry)
|
|
66
|
+
state._bump_version()
|
|
67
|
+
|
|
68
|
+
return agent_id, model_id
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SlowLoop — background optimization loop. Periodically re-reads SwarmMessage history
|
|
3
|
+
from the DB, rebalances UCB1 scores, and writes routing performance summaries to CausalGraph.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import asyncpg
|
|
14
|
+
|
|
15
|
+
from .bandit import HyperRouter, update_scores
|
|
16
|
+
from core.hyperstate.schema import AgentScore, ModelScore
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from memory.causal_graph import CausalGraph
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("hyperclaw.slow_loop")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SlowLoop:
|
|
25
|
+
"""
|
|
26
|
+
Background optimization loop.
|
|
27
|
+
|
|
28
|
+
Every interval_hours:
|
|
29
|
+
1. Reads all SwarmMessages from DB since last run
|
|
30
|
+
2. Recalculates UCB1 scores per agent/model per task_type
|
|
31
|
+
3. Updates HyperRouter in-place
|
|
32
|
+
4. Writes routing performance summary to CausalGraph (node_type='outcome')
|
|
33
|
+
5. Returns summary dict
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
hyper_router: HyperRouter,
|
|
39
|
+
pool: asyncpg.Pool,
|
|
40
|
+
causal_graph: "CausalGraph",
|
|
41
|
+
) -> None:
|
|
42
|
+
self._router = hyper_router
|
|
43
|
+
self._pool = pool
|
|
44
|
+
self._causal_graph = causal_graph
|
|
45
|
+
self._last_run: datetime | None = None
|
|
46
|
+
|
|
47
|
+
async def run_once(self) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Process SwarmMessages since last run, update router scores.
|
|
50
|
+
Returns summary: {messages_processed, agents_updated, models_updated, timestamp}
|
|
51
|
+
"""
|
|
52
|
+
since = self._last_run or datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
53
|
+
|
|
54
|
+
async with self._pool.acquire() as conn:
|
|
55
|
+
rows = await conn.fetch(
|
|
56
|
+
"""
|
|
57
|
+
SELECT agent_id, model_used, task_type, certified
|
|
58
|
+
FROM swarm_messages
|
|
59
|
+
WHERE created_at > $1
|
|
60
|
+
ORDER BY created_at ASC
|
|
61
|
+
""",
|
|
62
|
+
since,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
agents_seen: set[str] = set()
|
|
66
|
+
models_seen: set[str] = set()
|
|
67
|
+
messages_processed = 0
|
|
68
|
+
|
|
69
|
+
new_agent_scores: dict = dict(self._router.agent_scores)
|
|
70
|
+
new_model_scores: dict = dict(self._router.model_scores)
|
|
71
|
+
new_total = self._router.total_attempts
|
|
72
|
+
|
|
73
|
+
for row in rows:
|
|
74
|
+
agent_id = row["agent_id"]
|
|
75
|
+
model_id = row["model_used"]
|
|
76
|
+
task_type = row["task_type"]
|
|
77
|
+
# Treat certified=True as success; False as failure
|
|
78
|
+
success = bool(row["certified"])
|
|
79
|
+
|
|
80
|
+
# Update agent scores
|
|
81
|
+
if agent_id not in new_agent_scores:
|
|
82
|
+
new_agent_scores[agent_id] = AgentScore(task_type=task_type)
|
|
83
|
+
new_agent_scores[agent_id].attempts += 1
|
|
84
|
+
if success:
|
|
85
|
+
new_agent_scores[agent_id].successes += 1
|
|
86
|
+
agents_seen.add(agent_id)
|
|
87
|
+
|
|
88
|
+
# Update model scores (immutable pattern)
|
|
89
|
+
new_model_scores = update_scores(new_model_scores, model_id, task_type, success)
|
|
90
|
+
models_seen.add(model_id)
|
|
91
|
+
new_total += 1
|
|
92
|
+
messages_processed += 1
|
|
93
|
+
|
|
94
|
+
# Apply updated scores to router
|
|
95
|
+
self._router.agent_scores = new_agent_scores
|
|
96
|
+
self._router.model_scores = new_model_scores
|
|
97
|
+
self._router.total_attempts = new_total
|
|
98
|
+
|
|
99
|
+
now = datetime.now(timezone.utc)
|
|
100
|
+
self._last_run = now
|
|
101
|
+
|
|
102
|
+
summary = {
|
|
103
|
+
"messages_processed": messages_processed,
|
|
104
|
+
"agents_updated": len(agents_seen),
|
|
105
|
+
"models_updated": len(models_seen),
|
|
106
|
+
"timestamp": now.isoformat(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Write routing performance summary to CausalGraph
|
|
110
|
+
if messages_processed > 0:
|
|
111
|
+
try:
|
|
112
|
+
await self._causal_graph.add_node(
|
|
113
|
+
label=f"routing_optimization: processed {messages_processed} messages",
|
|
114
|
+
node_type="outcome",
|
|
115
|
+
domain="recursive",
|
|
116
|
+
metadata={
|
|
117
|
+
"slow_loop_summary": summary,
|
|
118
|
+
"agents_updated": list(agents_seen),
|
|
119
|
+
"models_updated": list(models_seen),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
log.warning(f"SlowLoop: failed to write CausalGraph summary: {e}")
|
|
124
|
+
|
|
125
|
+
log.info(
|
|
126
|
+
f"SlowLoop run_once: {messages_processed} messages, "
|
|
127
|
+
f"{len(agents_seen)} agents, {len(models_seen)} models updated"
|
|
128
|
+
)
|
|
129
|
+
return summary
|
|
130
|
+
|
|
131
|
+
async def run_forever(self, interval_hours: float = 6.0) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Infinite loop — runs run_once() then sleeps interval_hours.
|
|
134
|
+
Catches all exceptions, logs, continues.
|
|
135
|
+
"""
|
|
136
|
+
log.info(f"SlowLoop started — interval={interval_hours}h")
|
|
137
|
+
while True:
|
|
138
|
+
try:
|
|
139
|
+
summary = await self.run_once()
|
|
140
|
+
log.info(f"SlowLoop cycle complete: {summary}")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
log.error(f"SlowLoop error (continuing): {e}")
|
|
143
|
+
await asyncio.sleep(interval_hours * 3600)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .schema import (
|
|
2
|
+
HyperState,
|
|
3
|
+
Task,
|
|
4
|
+
Hypothesis,
|
|
5
|
+
ExperimentEntry,
|
|
6
|
+
AgentScore,
|
|
7
|
+
ModelScore,
|
|
8
|
+
CertifiedMethod,
|
|
9
|
+
RecursiveResearch,
|
|
10
|
+
Domain,
|
|
11
|
+
TaskType,
|
|
12
|
+
)
|
|
13
|
+
from .certifier import Certifier, CertificationError
|
|
14
|
+
from .store import HyperStateStore
|
|
15
|
+
from .state_manager import StateManager
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"HyperState", "Task", "Hypothesis", "ExperimentEntry",
|
|
19
|
+
"AgentScore", "ModelScore", "CertifiedMethod", "RecursiveResearch",
|
|
20
|
+
"Domain", "TaskType",
|
|
21
|
+
"Certifier", "CertificationError",
|
|
22
|
+
"HyperStateStore",
|
|
23
|
+
"StateManager",
|
|
24
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperClaw Certifier — validates ExperimentEntries before promoting to CertifiedMethod.
|
|
3
|
+
When a CausalGraph is provided, writes cause→effect relationships on every successful certification.
|
|
4
|
+
ChatJimmy outputs must always pass through Claude verification before reaching this stage.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from .schema import CertifiedMethod, ExperimentEntry
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from memory.causal_graph import CausalGraph
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CertificationError(Exception):
|
|
19
|
+
"""Raised when an ExperimentEntry fails certification criteria."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Certifier:
|
|
24
|
+
"""
|
|
25
|
+
Validates ExperimentEntry objects and produces CertifiedMethod records.
|
|
26
|
+
Optionally wires to CausalGraph — every certified method auto-writes a causal edge.
|
|
27
|
+
|
|
28
|
+
Certification rules:
|
|
29
|
+
1. test_trace must be non-empty
|
|
30
|
+
2. result must be non-empty
|
|
31
|
+
3. certified flag must be True on the entry
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, causal_graph: "CausalGraph | None" = None) -> None:
|
|
35
|
+
self._causal_graph = causal_graph
|
|
36
|
+
|
|
37
|
+
def certify(
|
|
38
|
+
self,
|
|
39
|
+
entry: ExperimentEntry,
|
|
40
|
+
domain: str = "business",
|
|
41
|
+
) -> CertifiedMethod:
|
|
42
|
+
"""
|
|
43
|
+
Validate an ExperimentEntry and return a CertifiedMethod.
|
|
44
|
+
If causal_graph was provided at init, schedules a causal edge write.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
CertificationError: if test_trace is empty, result is empty,
|
|
48
|
+
or certified flag is not set.
|
|
49
|
+
"""
|
|
50
|
+
if not entry.test_trace or not entry.test_trace.strip():
|
|
51
|
+
raise CertificationError(
|
|
52
|
+
f"Certification failed for method '{entry.method}': "
|
|
53
|
+
"test_trace is empty. A non-empty test trace is required."
|
|
54
|
+
)
|
|
55
|
+
if not entry.result or not entry.result.strip():
|
|
56
|
+
raise CertificationError(
|
|
57
|
+
f"Certification failed for method '{entry.method}': "
|
|
58
|
+
"result is empty. A non-empty result is required."
|
|
59
|
+
)
|
|
60
|
+
if not entry.certified:
|
|
61
|
+
raise CertificationError(
|
|
62
|
+
f"Certification failed for method '{entry.method}': "
|
|
63
|
+
"entry.certified is False. Entry must be verified before certification."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
certified_method = CertifiedMethod(
|
|
67
|
+
method_id=entry.method,
|
|
68
|
+
model_used=entry.model_used,
|
|
69
|
+
performance={
|
|
70
|
+
"cost_usd": entry.cost_usd,
|
|
71
|
+
"latency_ms": entry.latency_ms,
|
|
72
|
+
"test_trace_length": len(entry.test_trace),
|
|
73
|
+
"result_length": len(entry.result),
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Write causal edge if CausalGraph is wired in
|
|
78
|
+
if self._causal_graph is not None:
|
|
79
|
+
self._schedule_causal_write(entry, domain, certified_method)
|
|
80
|
+
|
|
81
|
+
return certified_method
|
|
82
|
+
|
|
83
|
+
def _schedule_causal_write(
|
|
84
|
+
self,
|
|
85
|
+
entry: ExperimentEntry,
|
|
86
|
+
domain: str,
|
|
87
|
+
certified_method: CertifiedMethod,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Fire-and-forget coroutine to write causal edge.
|
|
91
|
+
Uses asyncio.ensure_future if an event loop is running,
|
|
92
|
+
otherwise stores for deferred execution.
|
|
93
|
+
"""
|
|
94
|
+
import asyncio
|
|
95
|
+
|
|
96
|
+
coro = self._write_causal_edge(entry, domain, certified_method)
|
|
97
|
+
try:
|
|
98
|
+
loop = asyncio.get_running_loop()
|
|
99
|
+
loop.create_task(coro)
|
|
100
|
+
except RuntimeError:
|
|
101
|
+
# No running event loop — skip (sync context, e.g. unit tests)
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
async def _write_causal_edge(
|
|
105
|
+
self,
|
|
106
|
+
entry: ExperimentEntry,
|
|
107
|
+
domain: str,
|
|
108
|
+
certified_method: CertifiedMethod,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Async: write cause→effect to CausalGraph."""
|
|
111
|
+
try:
|
|
112
|
+
method_uuid = UUID(certified_method.method_id) if _is_uuid(certified_method.method_id) else uuid.uuid4()
|
|
113
|
+
await self._causal_graph.write_certified_method( # type: ignore[union-attr]
|
|
114
|
+
method_description=entry.method,
|
|
115
|
+
cause_description=f"method: {entry.method}",
|
|
116
|
+
effect_description=f"result: {entry.result}",
|
|
117
|
+
domain=domain,
|
|
118
|
+
confidence=1.0,
|
|
119
|
+
method_id=method_uuid,
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
import logging
|
|
123
|
+
logging.getLogger("hyperclaw.certifier").warning(
|
|
124
|
+
f"CausalGraph write failed for method '{entry.method}': {e}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def validate_batch(
|
|
128
|
+
self,
|
|
129
|
+
entries: list[ExperimentEntry],
|
|
130
|
+
domain: str = "business",
|
|
131
|
+
) -> tuple[list[CertifiedMethod], list[CertificationError]]:
|
|
132
|
+
"""
|
|
133
|
+
Attempt to certify a batch of entries. Returns (certified, errors).
|
|
134
|
+
Does not raise — all errors are collected and returned.
|
|
135
|
+
"""
|
|
136
|
+
certified: list[CertifiedMethod] = []
|
|
137
|
+
errors: list[CertificationError] = []
|
|
138
|
+
|
|
139
|
+
for entry in entries:
|
|
140
|
+
try:
|
|
141
|
+
cm = self.certify(entry, domain=domain)
|
|
142
|
+
certified.append(cm)
|
|
143
|
+
except CertificationError as e:
|
|
144
|
+
errors.append(e)
|
|
145
|
+
|
|
146
|
+
return certified, errors
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _is_uuid(value: str) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
UUID(value)
|
|
152
|
+
return True
|
|
153
|
+
except (ValueError, AttributeError):
|
|
154
|
+
return False
|