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
models/claude_client.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ClaudeClient — async Anthropic API client with retry logic, streaming, and cost tracking.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import AsyncIterator
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import anthropic
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger("hyperclaw.claude_client")
|
|
16
|
+
|
|
17
|
+
# Cost per 1K tokens (approximate, subject to Anthropic pricing changes)
|
|
18
|
+
_INPUT_COST_PER_1K = 0.003
|
|
19
|
+
_OUTPUT_COST_PER_1K = 0.015
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaudeClient:
|
|
23
|
+
"""
|
|
24
|
+
Async Anthropic Claude client.
|
|
25
|
+
|
|
26
|
+
Retries up to 3 times with exponential backoff on transient errors.
|
|
27
|
+
Logs every call: model, token counts, latency, and cost estimate.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str,
|
|
33
|
+
model: str = "claude-sonnet-4-6",
|
|
34
|
+
max_tokens: int | None = None,
|
|
35
|
+
base_url: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
from hyperclaw.cost_guard import guard_client, max_output
|
|
38
|
+
resolved_base_url = base_url or os.environ.get("OPENAI_BASE_URL")
|
|
39
|
+
prov = os.environ.get("LLM_PROVIDER", "anthropic")
|
|
40
|
+
self._provider = prov
|
|
41
|
+
if prov == "bedrock":
|
|
42
|
+
pass # no client needed — uses boto3 per-call
|
|
43
|
+
elif prov == "openai_compat":
|
|
44
|
+
import openai as _oai
|
|
45
|
+
self._oai_client = guard_client(_oai.AsyncOpenAI(
|
|
46
|
+
api_key=api_key or os.environ.get("OPENAI_API_KEY", "placeholder"),
|
|
47
|
+
base_url=resolved_base_url or os.environ.get("OPENAI_BASE_URL"),
|
|
48
|
+
))
|
|
49
|
+
else:
|
|
50
|
+
# anthropic or anthropic_compat
|
|
51
|
+
kwargs: dict = {"api_key": api_key or None}
|
|
52
|
+
base = resolved_base_url or os.environ.get("ANTHROPIC_BASE_URL")
|
|
53
|
+
if base:
|
|
54
|
+
kwargs["base_url"] = base
|
|
55
|
+
self._client = guard_client(anthropic.AsyncAnthropic(**kwargs), "anthropic")
|
|
56
|
+
self.model = model
|
|
57
|
+
# explicit max output on every call (cost guard path "chat")
|
|
58
|
+
self.max_tokens = max_output("chat", "anthropic", model, requested=max_tokens)
|
|
59
|
+
|
|
60
|
+
async def chat(
|
|
61
|
+
self,
|
|
62
|
+
messages: list[dict],
|
|
63
|
+
system: str = "",
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Send a chat request with retry logic (3 attempts, exponential backoff).
|
|
67
|
+
Returns the assistant's text response.
|
|
68
|
+
"""
|
|
69
|
+
last_error: Exception | None = None
|
|
70
|
+
for attempt in range(3):
|
|
71
|
+
try:
|
|
72
|
+
return await self._do_chat(messages, system)
|
|
73
|
+
except (anthropic.APIConnectionError, anthropic.APITimeoutError) as e:
|
|
74
|
+
last_error = e
|
|
75
|
+
wait = 2 ** attempt
|
|
76
|
+
log.warning(f"Claude transient error (attempt {attempt+1}/3): {e}. Retrying in {wait}s...")
|
|
77
|
+
await asyncio.sleep(wait)
|
|
78
|
+
except anthropic.RateLimitError as e:
|
|
79
|
+
last_error = e
|
|
80
|
+
wait = 4 ** attempt
|
|
81
|
+
log.warning(f"Claude rate limit (attempt {attempt+1}/3). Retrying in {wait}s...")
|
|
82
|
+
await asyncio.sleep(wait)
|
|
83
|
+
raise last_error or RuntimeError("Claude chat failed after 3 attempts")
|
|
84
|
+
|
|
85
|
+
async def _do_chat(self, messages: list[dict], system: str) -> str:
|
|
86
|
+
if self._provider == "bedrock":
|
|
87
|
+
return await self._do_chat_bedrock(messages, system)
|
|
88
|
+
if self._provider == "openai_compat":
|
|
89
|
+
return await self._do_chat_openai(messages, system)
|
|
90
|
+
t0 = time.time()
|
|
91
|
+
kwargs: dict = dict(
|
|
92
|
+
model=self.model,
|
|
93
|
+
max_tokens=self.max_tokens,
|
|
94
|
+
messages=messages,
|
|
95
|
+
)
|
|
96
|
+
if system:
|
|
97
|
+
kwargs["system"] = system
|
|
98
|
+
|
|
99
|
+
response = await self._client.messages.create(**kwargs)
|
|
100
|
+
latency_ms = (time.time() - t0) * 1000
|
|
101
|
+
|
|
102
|
+
input_tokens = response.usage.input_tokens
|
|
103
|
+
output_tokens = response.usage.output_tokens
|
|
104
|
+
cost = (input_tokens / 1000 * _INPUT_COST_PER_1K) + (output_tokens / 1000 * _OUTPUT_COST_PER_1K)
|
|
105
|
+
# Handle text and thinking content blocks
|
|
106
|
+
text = ""
|
|
107
|
+
for block in (response.content or []):
|
|
108
|
+
if hasattr(block, "type") and block.type == "text":
|
|
109
|
+
text = block.text
|
|
110
|
+
break
|
|
111
|
+
if not text and response.content:
|
|
112
|
+
text = getattr(response.content[0], "text", "") or ""
|
|
113
|
+
|
|
114
|
+
log.info(
|
|
115
|
+
f"Claude call | model={self.model} | "
|
|
116
|
+
f"in={input_tokens} out={output_tokens} | "
|
|
117
|
+
f"latency={latency_ms:.0f}ms | cost=${cost:.5f}"
|
|
118
|
+
)
|
|
119
|
+
return text
|
|
120
|
+
|
|
121
|
+
async def _do_chat_bedrock(self, messages: list[dict], system: str) -> str:
|
|
122
|
+
try:
|
|
123
|
+
from core.providers.llm import BedrockProvider, Message as BMsg
|
|
124
|
+
except ImportError:
|
|
125
|
+
import sys as _sys, pathlib as _pl
|
|
126
|
+
_sys.path.insert(0, str(_pl.Path(__file__).parent.parent))
|
|
127
|
+
from core.providers.llm import BedrockProvider, Message as BMsg
|
|
128
|
+
t0 = time.time()
|
|
129
|
+
provider = BedrockProvider(
|
|
130
|
+
access_key=os.environ.get("AWS_ACCESS_KEY_ID"),
|
|
131
|
+
secret_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
|
132
|
+
region=os.environ.get("AWS_REGION", "us-east-1"),
|
|
133
|
+
)
|
|
134
|
+
bedrock_model = os.environ.get("BEDROCK_MODEL", "anthropic.claude-sonnet-5")
|
|
135
|
+
bmsgs = [BMsg(role=m["role"], content=m["content"]) for m in messages]
|
|
136
|
+
response = await provider.complete(
|
|
137
|
+
messages=bmsgs,
|
|
138
|
+
model=bedrock_model,
|
|
139
|
+
max_tokens=self.max_tokens,
|
|
140
|
+
system=system or None,
|
|
141
|
+
)
|
|
142
|
+
latency_ms = (time.time() - t0) * 1000
|
|
143
|
+
log.info(
|
|
144
|
+
f"Bedrock call | model={bedrock_model} | "
|
|
145
|
+
f"in={response.input_tokens} out={response.output_tokens} | "
|
|
146
|
+
f"latency={latency_ms:.0f}ms"
|
|
147
|
+
)
|
|
148
|
+
return response.content
|
|
149
|
+
|
|
150
|
+
async def _do_chat_openai(self, messages: list[dict], system: str) -> str:
|
|
151
|
+
t0 = time.time()
|
|
152
|
+
oai_messages = []
|
|
153
|
+
if system:
|
|
154
|
+
oai_messages.append({"role": "system", "content": system})
|
|
155
|
+
oai_messages.extend(messages)
|
|
156
|
+
model = os.environ.get("OPENAI_MODEL", self.model)
|
|
157
|
+
try:
|
|
158
|
+
response = await self._oai_client.chat.completions.create(
|
|
159
|
+
model=model,
|
|
160
|
+
messages=oai_messages,
|
|
161
|
+
max_tokens=self.max_tokens,
|
|
162
|
+
)
|
|
163
|
+
except Exception as _e:
|
|
164
|
+
_err = str(_e).lower()
|
|
165
|
+
if any(k in _err for k in ("connection", "connect", "unreachable", "refused", "timeout")):
|
|
166
|
+
log.warning(f"OpenAI-compat unreachable ({_e}), falling back to Bedrock")
|
|
167
|
+
import re as _re
|
|
168
|
+
from pathlib import Path as _Path
|
|
169
|
+
_bedrock_model = os.environ.get("BEDROCK_MODEL", "us.anthropic.claude-sonnet-5")
|
|
170
|
+
os.environ["LLM_PROVIDER"] = "bedrock"
|
|
171
|
+
os.environ.setdefault("AWS_REGION", "us-west-2")
|
|
172
|
+
os.environ.setdefault("AWS_PROFILE", "hyper")
|
|
173
|
+
os.environ["BEDROCK_MODEL"] = _bedrock_model
|
|
174
|
+
_env_f = _Path.home() / ".hyperclaw" / ".env"
|
|
175
|
+
if _env_f.exists():
|
|
176
|
+
_txt = _env_f.read_text()
|
|
177
|
+
_txt = _re.sub(r'^LLM_PROVIDER=.*$', 'LLM_PROVIDER=bedrock', _txt, flags=_re.MULTILINE)
|
|
178
|
+
_env_f.write_text(_txt)
|
|
179
|
+
self._provider = "bedrock"
|
|
180
|
+
return await self._do_chat_bedrock(messages, system)
|
|
181
|
+
raise
|
|
182
|
+
latency_ms = (time.time() - t0) * 1000
|
|
183
|
+
usage = response.usage
|
|
184
|
+
log.info(
|
|
185
|
+
f"OpenAI-compat call | model={model} | "
|
|
186
|
+
f"in={usage.prompt_tokens if usage else '?'} "
|
|
187
|
+
f"out={usage.completion_tokens if usage else '?'} | "
|
|
188
|
+
f"latency={latency_ms:.0f}ms"
|
|
189
|
+
)
|
|
190
|
+
return response.choices[0].message.content or ""
|
|
191
|
+
|
|
192
|
+
async def chat_stream(
|
|
193
|
+
self,
|
|
194
|
+
messages: list[dict],
|
|
195
|
+
system: str = "",
|
|
196
|
+
) -> AsyncIterator[str]:
|
|
197
|
+
"""
|
|
198
|
+
Stream a chat response. Yields text chunks as they arrive.
|
|
199
|
+
"""
|
|
200
|
+
if self._provider == "bedrock":
|
|
201
|
+
# BedrockProvider has no streaming; yield full response as single chunk
|
|
202
|
+
text = await self._do_chat_bedrock(messages, system)
|
|
203
|
+
yield text
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if self._provider == "openai_compat":
|
|
207
|
+
oai_messages = []
|
|
208
|
+
if system:
|
|
209
|
+
oai_messages.append({"role": "system", "content": system})
|
|
210
|
+
oai_messages.extend(messages)
|
|
211
|
+
model = os.environ.get("OPENAI_MODEL", self.model)
|
|
212
|
+
async with self._oai_client.chat.completions.create(
|
|
213
|
+
model=model,
|
|
214
|
+
messages=oai_messages,
|
|
215
|
+
max_tokens=self.max_tokens,
|
|
216
|
+
stream=True,
|
|
217
|
+
) as stream:
|
|
218
|
+
async for chunk in stream:
|
|
219
|
+
delta = chunk.choices[0].delta.content if chunk.choices else None
|
|
220
|
+
if delta:
|
|
221
|
+
yield delta
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# anthropic / anthropic_compat
|
|
225
|
+
kwargs: dict = dict(
|
|
226
|
+
model=self.model,
|
|
227
|
+
max_tokens=self.max_tokens,
|
|
228
|
+
messages=messages,
|
|
229
|
+
)
|
|
230
|
+
if system:
|
|
231
|
+
kwargs["system"] = system
|
|
232
|
+
|
|
233
|
+
async with self._client.messages.stream(**kwargs) as stream:
|
|
234
|
+
async for text in stream.text_stream:
|
|
235
|
+
yield text
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ClaudeCodeSubagent — empirical code certification loop.
|
|
3
|
+
Generates code via Claude, executes it in subprocess, iterates on failures.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from core.hyperstate.certifier import CertificationError, Certifier
|
|
14
|
+
from core.hyperstate.schema import CertifiedMethod, ExperimentEntry
|
|
15
|
+
from .claude_client import ClaudeClient
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("hyperclaw.claude_code_subagent")
|
|
18
|
+
|
|
19
|
+
_CODE_BLOCK_RE = re.compile(r"```(?:python)?\n(.*?)```", re.DOTALL)
|
|
20
|
+
|
|
21
|
+
_GENERATE_SYSTEM = """You are a precise Python code generator.
|
|
22
|
+
When given a task, output ONLY a complete, runnable Python script in a ```python code block.
|
|
23
|
+
Include inline tests using assert statements or a simple main() function.
|
|
24
|
+
The code must be self-contained and executable."""
|
|
25
|
+
|
|
26
|
+
_FIX_SYSTEM = """You are a Python debugging expert.
|
|
27
|
+
You will be given code that failed along with its error output.
|
|
28
|
+
Output ONLY the fixed, complete, runnable Python script in a ```python code block.
|
|
29
|
+
Do not explain — just fix the code."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SubagentResult:
|
|
34
|
+
code: str
|
|
35
|
+
test_trace: str
|
|
36
|
+
success: bool
|
|
37
|
+
iterations: int
|
|
38
|
+
total_time_seconds: float
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ClaudeCodeSubagent:
|
|
43
|
+
"""
|
|
44
|
+
Iterative code generation and certification agent.
|
|
45
|
+
|
|
46
|
+
Workflow:
|
|
47
|
+
1. Ask Claude to generate code for a task
|
|
48
|
+
2. Execute generated code in a subprocess (30s timeout)
|
|
49
|
+
3. If tests pass → return success
|
|
50
|
+
4. If tests fail → feed error back to Claude with "Fix this:" prompt
|
|
51
|
+
5. Repeat up to max_iterations
|
|
52
|
+
6. On final failure → return SubagentResult(success=False)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, claude_client: ClaudeClient) -> None:
|
|
56
|
+
self._claude = claude_client
|
|
57
|
+
self._certifier = Certifier()
|
|
58
|
+
|
|
59
|
+
async def run(
|
|
60
|
+
self,
|
|
61
|
+
task: str,
|
|
62
|
+
context: str = "",
|
|
63
|
+
max_iterations: int = 5,
|
|
64
|
+
) -> SubagentResult:
|
|
65
|
+
"""
|
|
66
|
+
Run the code generation → execution → fix loop.
|
|
67
|
+
Returns a SubagentResult with success/failure and full test trace.
|
|
68
|
+
"""
|
|
69
|
+
t0 = time.time()
|
|
70
|
+
messages: list[dict] = []
|
|
71
|
+
code = ""
|
|
72
|
+
test_trace = ""
|
|
73
|
+
last_error: str | None = None
|
|
74
|
+
|
|
75
|
+
# Initial generation request
|
|
76
|
+
user_msg = f"Task: {task}"
|
|
77
|
+
if context:
|
|
78
|
+
user_msg += f"\n\nContext:\n{context}"
|
|
79
|
+
messages.append({"role": "user", "content": user_msg})
|
|
80
|
+
|
|
81
|
+
for iteration in range(1, max_iterations + 1):
|
|
82
|
+
log.info(f"Subagent iteration {iteration}/{max_iterations}")
|
|
83
|
+
|
|
84
|
+
# Generate / fix
|
|
85
|
+
response = await self._claude.chat(messages, system=_GENERATE_SYSTEM if iteration == 1 else _FIX_SYSTEM)
|
|
86
|
+
messages.append({"role": "assistant", "content": response})
|
|
87
|
+
|
|
88
|
+
# Extract code block
|
|
89
|
+
code = _extract_code(response)
|
|
90
|
+
if not code:
|
|
91
|
+
last_error = "No code block found in Claude response"
|
|
92
|
+
messages.append({"role": "user", "content": f"Fix this: {last_error}\n\nOutput a complete Python script in a ```python block."})
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Execute
|
|
96
|
+
stdout, stderr, returncode = await _run_subprocess(code, timeout=30)
|
|
97
|
+
test_trace = f"=== Iteration {iteration} ===\n{stdout}"
|
|
98
|
+
if stderr:
|
|
99
|
+
test_trace += f"\n--- stderr ---\n{stderr}"
|
|
100
|
+
test_trace += f"\n--- exit code: {returncode} ---"
|
|
101
|
+
|
|
102
|
+
if returncode == 0:
|
|
103
|
+
total_time = time.time() - t0
|
|
104
|
+
log.info(f"Subagent succeeded on iteration {iteration} in {total_time:.1f}s")
|
|
105
|
+
return SubagentResult(
|
|
106
|
+
code=code,
|
|
107
|
+
test_trace=test_trace,
|
|
108
|
+
success=True,
|
|
109
|
+
iterations=iteration,
|
|
110
|
+
total_time_seconds=total_time,
|
|
111
|
+
error=None,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
last_error = f"Exit code {returncode}\nstdout: {stdout}\nstderr: {stderr}"
|
|
115
|
+
messages.append({
|
|
116
|
+
"role": "user",
|
|
117
|
+
"content": f"Fix this:\n\n{last_error}\n\nOutput the complete fixed Python script in a ```python block.",
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
total_time = time.time() - t0
|
|
121
|
+
log.warning(f"Subagent exhausted {max_iterations} iterations. Last error: {last_error}")
|
|
122
|
+
return SubagentResult(
|
|
123
|
+
code=code,
|
|
124
|
+
test_trace=test_trace,
|
|
125
|
+
success=False,
|
|
126
|
+
iterations=max_iterations,
|
|
127
|
+
total_time_seconds=total_time,
|
|
128
|
+
error=last_error,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def certify(
|
|
132
|
+
self,
|
|
133
|
+
task: str,
|
|
134
|
+
context: str = "",
|
|
135
|
+
) -> tuple[str, CertifiedMethod]:
|
|
136
|
+
"""
|
|
137
|
+
Run the subagent loop and certify the result if successful.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
(code, CertifiedMethod) on success
|
|
141
|
+
Raises:
|
|
142
|
+
CertificationError if the run failed or certification criteria not met
|
|
143
|
+
"""
|
|
144
|
+
result = await self.run(task, context)
|
|
145
|
+
if not result.success:
|
|
146
|
+
raise CertificationError(
|
|
147
|
+
f"Subagent failed after {result.iterations} iterations: {result.error}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
entry = ExperimentEntry(
|
|
151
|
+
method=f"claude_code_subagent:{task[:60]}",
|
|
152
|
+
model_used=self._claude.model,
|
|
153
|
+
result=result.code[:500],
|
|
154
|
+
certified=True,
|
|
155
|
+
test_trace=result.test_trace,
|
|
156
|
+
cost_usd=0.0,
|
|
157
|
+
latency_ms=result.total_time_seconds * 1000,
|
|
158
|
+
)
|
|
159
|
+
certified_method = self._certifier.certify(entry)
|
|
160
|
+
return result.code, certified_method
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
def _extract_code(text: str) -> str:
|
|
166
|
+
"""Extract the first Python code block from a markdown-formatted response."""
|
|
167
|
+
match = _CODE_BLOCK_RE.search(text)
|
|
168
|
+
if match:
|
|
169
|
+
return match.group(1).strip()
|
|
170
|
+
# Fallback: return the whole response if it looks like code
|
|
171
|
+
if text.strip().startswith(("def ", "import ", "class ", "#")):
|
|
172
|
+
return text.strip()
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def _run_subprocess(code: str, timeout: float = 30.0) -> tuple[str, str, int]:
|
|
177
|
+
"""
|
|
178
|
+
Execute Python code in a subprocess. Returns (stdout, stderr, returncode).
|
|
179
|
+
Times out after `timeout` seconds.
|
|
180
|
+
"""
|
|
181
|
+
proc = await asyncio.create_subprocess_exec(
|
|
182
|
+
"python3", "-c", code,
|
|
183
|
+
stdout=asyncio.subprocess.PIPE,
|
|
184
|
+
stderr=asyncio.subprocess.PIPE,
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
188
|
+
return stdout_b.decode(), stderr_b.decode(), proc.returncode or 0
|
|
189
|
+
except asyncio.TimeoutError:
|
|
190
|
+
proc.kill()
|
|
191
|
+
await proc.communicate()
|
|
192
|
+
return "", f"Process timed out after {timeout}s", 1
|
models/openai_client.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAIClient — async OpenAI-compatible client supporting custom base_url.
|
|
3
|
+
Works with OpenAI, Azure OpenAI, local vLLM, LiteLLM, Ollama, and any
|
|
4
|
+
endpoint that implements the OpenAI chat completions API.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import AsyncIterator
|
|
12
|
+
|
|
13
|
+
from openai import AsyncOpenAI, APIConnectionError, APITimeoutError, RateLimitError
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger("hyperclaw.openai_client")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenAIClient:
|
|
19
|
+
"""
|
|
20
|
+
Async OpenAI-compatible client with retry logic and cost tracking.
|
|
21
|
+
|
|
22
|
+
Pass base_url to point at any OpenAI-compatible endpoint:
|
|
23
|
+
- Azure OpenAI: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
24
|
+
- vLLM: http://localhost:8000/v1
|
|
25
|
+
- LiteLLM: http://localhost:4000/v1
|
|
26
|
+
- Ollama: http://localhost:11434/v1
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_key: str,
|
|
32
|
+
model: str = "gpt-4o",
|
|
33
|
+
max_tokens: int | None = None,
|
|
34
|
+
base_url: str | None = None,
|
|
35
|
+
cost_per_1k_input: float = 0.005,
|
|
36
|
+
cost_per_1k_output: float = 0.015,
|
|
37
|
+
) -> None:
|
|
38
|
+
from hyperclaw.cost_guard import guard_client, max_output
|
|
39
|
+
self._client = guard_client(AsyncOpenAI(
|
|
40
|
+
api_key=api_key,
|
|
41
|
+
base_url=base_url,
|
|
42
|
+
))
|
|
43
|
+
self.model = model
|
|
44
|
+
# explicit max output on every call (cost guard path "chat")
|
|
45
|
+
self.max_tokens = max_output("chat", "openai", model, requested=max_tokens)
|
|
46
|
+
self._cost_in = cost_per_1k_input
|
|
47
|
+
self._cost_out = cost_per_1k_output
|
|
48
|
+
|
|
49
|
+
async def chat(
|
|
50
|
+
self,
|
|
51
|
+
messages: list[dict],
|
|
52
|
+
system: str = "",
|
|
53
|
+
) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Send a chat request with retry logic (3 attempts, exponential backoff).
|
|
56
|
+
Returns the assistant's text response.
|
|
57
|
+
"""
|
|
58
|
+
full_messages = []
|
|
59
|
+
if system:
|
|
60
|
+
full_messages.append({"role": "system", "content": system})
|
|
61
|
+
full_messages.extend(messages)
|
|
62
|
+
|
|
63
|
+
last_error: Exception | None = None
|
|
64
|
+
for attempt in range(3):
|
|
65
|
+
try:
|
|
66
|
+
return await self._do_chat(full_messages)
|
|
67
|
+
except (APIConnectionError, APITimeoutError) as e:
|
|
68
|
+
last_error = e
|
|
69
|
+
wait = 2 ** attempt
|
|
70
|
+
log.warning(f"OpenAI transient error (attempt {attempt+1}/3): {e}. Retrying in {wait}s...")
|
|
71
|
+
await asyncio.sleep(wait)
|
|
72
|
+
except RateLimitError as e:
|
|
73
|
+
last_error = e
|
|
74
|
+
wait = 4 ** attempt
|
|
75
|
+
log.warning(f"OpenAI rate limit (attempt {attempt+1}/3). Retrying in {wait}s...")
|
|
76
|
+
await asyncio.sleep(wait)
|
|
77
|
+
raise last_error or RuntimeError("OpenAI chat failed after 3 attempts")
|
|
78
|
+
|
|
79
|
+
async def _do_chat(self, messages: list[dict]) -> str:
|
|
80
|
+
t0 = time.time()
|
|
81
|
+
response = await self._client.chat.completions.create(
|
|
82
|
+
model=self.model,
|
|
83
|
+
max_tokens=self.max_tokens,
|
|
84
|
+
messages=messages,
|
|
85
|
+
)
|
|
86
|
+
latency_ms = (time.time() - t0) * 1000
|
|
87
|
+
|
|
88
|
+
input_tokens = response.usage.prompt_tokens if response.usage else 0
|
|
89
|
+
output_tokens = response.usage.completion_tokens if response.usage else 0
|
|
90
|
+
cost = (input_tokens / 1000 * self._cost_in) + (output_tokens / 1000 * self._cost_out)
|
|
91
|
+
text = response.choices[0].message.content or "" if response.choices else ""
|
|
92
|
+
|
|
93
|
+
log.info(
|
|
94
|
+
f"OpenAI call | model={self.model} | "
|
|
95
|
+
f"in={input_tokens} out={output_tokens} | "
|
|
96
|
+
f"latency={latency_ms:.0f}ms | cost=${cost:.5f}"
|
|
97
|
+
)
|
|
98
|
+
return text
|
|
99
|
+
|
|
100
|
+
async def chat_stream(
|
|
101
|
+
self,
|
|
102
|
+
messages: list[dict],
|
|
103
|
+
system: str = "",
|
|
104
|
+
) -> AsyncIterator[str]:
|
|
105
|
+
"""Stream a chat response. Yields text chunks as they arrive."""
|
|
106
|
+
full_messages = []
|
|
107
|
+
if system:
|
|
108
|
+
full_messages.append({"role": "system", "content": system})
|
|
109
|
+
full_messages.extend(messages)
|
|
110
|
+
|
|
111
|
+
stream = await self._client.chat.completions.create(
|
|
112
|
+
model=self.model,
|
|
113
|
+
max_tokens=self.max_tokens,
|
|
114
|
+
messages=full_messages,
|
|
115
|
+
stream=True,
|
|
116
|
+
)
|
|
117
|
+
async for chunk in stream:
|
|
118
|
+
delta = chunk.choices[0].delta.content if chunk.choices else None
|
|
119
|
+
if delta:
|
|
120
|
+
yield delta
|