monkeybot 2.1.1__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.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Load ``monkeybot_config/monkeybot.yaml`` into ``os.environ`` for unset keys only.
|
|
2
|
+
|
|
3
|
+
Precedence: existing process environment (including ``.env`` via dotenv) wins.
|
|
4
|
+
Discovery: ``MONKEYBOT_CONFIG`` if set, else ``<cwd>/monkeybot_config/monkeybot.yaml``.
|
|
5
|
+
Optional ``includes``: list of paths relative to the primary config file's directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_RUNTIME_ENV_APPLIED = False
|
|
21
|
+
|
|
22
|
+
# (section, key) -> os.environ name
|
|
23
|
+
# YAML gcp/anthropic_vertex project ids map here; prefer GOOGLE_CLOUD_PROJECT when set in .env.
|
|
24
|
+
_GCP_PROJECT_ENV_KEYS = frozenset({"VERTEX_AI_PROJECT_ID", "ANTHROPIC_VERTEX_PROJECT_ID"})
|
|
25
|
+
|
|
26
|
+
# Public alias for CLI / validation (stable API).
|
|
27
|
+
ENV_MAP: dict[tuple[str, str], str] = {
|
|
28
|
+
("runtime", "log_level"): "LOG_LEVEL",
|
|
29
|
+
("runtime", "port"): "PORT",
|
|
30
|
+
("runtime", "gateway_port"): "GATEWAY_PORT",
|
|
31
|
+
("paths", "agent_md"): "AGENT_MD",
|
|
32
|
+
("paths", "memory_path"): "MEMORY_PATH",
|
|
33
|
+
("paths", "memory_storage_uri"): "MEMORY_STORAGE_URI",
|
|
34
|
+
("paths", "skills_path"): "SKILLS_PATH",
|
|
35
|
+
("paths", "db_url"): "DB_URL",
|
|
36
|
+
("paths", "mcp_config"): "MCP_CONFIG",
|
|
37
|
+
("paths", "command_allowlist_config"): "COMMAND_ALLOWLIST_CONFIG",
|
|
38
|
+
("paths", "permission_config"): "PERMISSION_CONFIG",
|
|
39
|
+
("paths", "workspace_root"): "MONKEYBOT_WORKSPACE_ROOT",
|
|
40
|
+
("model", "provider"): "MODEL_PROVIDER",
|
|
41
|
+
("model", "name"): "MODEL_NAME",
|
|
42
|
+
("model", "temperature"): "MODEL_TEMPERATURE",
|
|
43
|
+
("model", "max_tokens"): "MODEL_MAX_TOKENS",
|
|
44
|
+
("model", "thinking_budget"): "MODEL_THINKING_BUDGET",
|
|
45
|
+
("model", "context_window"): "MODEL_CONTEXT_WINDOW",
|
|
46
|
+
("model", "summarization_model"): "CONTEXT_SUMMARIZATION_MODEL",
|
|
47
|
+
("model", "max_turns"): "MAX_TURNS",
|
|
48
|
+
("model", "cache_retention"): "MODEL_CACHE_RETENTION",
|
|
49
|
+
("gcp", "project_id"): "VERTEX_AI_PROJECT_ID",
|
|
50
|
+
("gcp", "location"): "VERTEX_AI_LOCATION",
|
|
51
|
+
("anthropic_vertex", "project_id"): "ANTHROPIC_VERTEX_PROJECT_ID",
|
|
52
|
+
("anthropic_vertex", "region"): "ANTHROPIC_VERTEX_REGION",
|
|
53
|
+
("gateway", "pending_response_timeout_sec"): "PENDING_RESPONSE_TIMEOUT_SEC",
|
|
54
|
+
("gateway", "sse_replay_max"): "SSE_REPLAY_MAX",
|
|
55
|
+
("gateway", "graceful_shutdown_timeout_sec"): "GRACEFUL_SHUTDOWN_TIMEOUT_SEC",
|
|
56
|
+
("gateway", "cors_allow_origins"): "MONKEYBOT_CORS_ALLOW_ORIGINS",
|
|
57
|
+
("context_curation", "enabled"): "CONTEXT_CURATION_ENABLED",
|
|
58
|
+
("context_curation", "memory_window_lines"): "CONTEXT_CURATION_MEMORY_WINDOW_LINES",
|
|
59
|
+
("context_curation", "memory_index_cap"): "MEMORY_INDEX_CAP",
|
|
60
|
+
("context_curation", "memory_token_threshold"): "CONTEXT_CURATION_MEMORY_TOKEN_THRESHOLD",
|
|
61
|
+
("context_curation", "curator_model"): "CONTEXT_CURATOR_MODEL",
|
|
62
|
+
("context_curation", "timeout_sec"): "CONTEXT_CURATION_TIMEOUT_SEC",
|
|
63
|
+
("memory_hook", "enabled"): "MONKEYBOT_MEMORY_HOOK_ENABLED",
|
|
64
|
+
("subagent", "timeout_sec"): "SUBAGENT_TIMEOUT_SEC",
|
|
65
|
+
("subagent", "max_turns"): "SUBAGENT_MAX_TURNS",
|
|
66
|
+
("subagent", "agent_md"): "MONKEYBOT_SUBAGENT_AGENT_MD",
|
|
67
|
+
("tools", "denied_patterns"): "MONKEYBOT_TOOL_DENIED_PATTERNS",
|
|
68
|
+
("tools", "read_max_lines"): "MONKEYBOT_READ_MAX_LINES",
|
|
69
|
+
("tools", "read_default_lines"): "MONKEYBOT_READ_DEFAULT_LINES",
|
|
70
|
+
("tools", "spill_read_max_lines"): "MONKEYBOT_SPILL_READ_MAX_LINES",
|
|
71
|
+
("tools", "spill_min_chars"): "MONKEYBOT_SPILL_MIN_CHARS",
|
|
72
|
+
("tools", "result_budget_fraction"): "MONKEYBOT_RESULT_BUDGET_FRACTION",
|
|
73
|
+
("tools", "result_budget_floor_tokens"): "MONKEYBOT_RESULT_BUDGET_FLOOR_TOKENS",
|
|
74
|
+
("compression", "light_ratio"): "MONKEYBOT_PRESSURE_LIGHT_RATIO",
|
|
75
|
+
("compression", "moderate_ratio"): "MONKEYBOT_PRESSURE_MODERATE_RATIO",
|
|
76
|
+
("compression", "aggressive_ratio"): "MONKEYBOT_PRESSURE_AGGRESSIVE_RATIO",
|
|
77
|
+
("compression", "resume_thinking_budget"): "MONKEYBOT_RESUME_THINKING_BUDGET",
|
|
78
|
+
("web_search", "backend"): "WEB_SEARCH_BACKEND",
|
|
79
|
+
("web_search", "max_results"): "WEB_SEARCH_MAX_RESULTS",
|
|
80
|
+
("sandbox", "enabled"): "SANDBOX_ENABLED",
|
|
81
|
+
("sandbox", "server_url"): "SANDBOX_SERVER_URL",
|
|
82
|
+
("sandbox", "image"): "SANDBOX_IMAGE",
|
|
83
|
+
("sandbox", "ttl_seconds"): "SANDBOX_TTL_SECONDS",
|
|
84
|
+
("fake_provider", "events_json"): "MONKEYBOT_FAKE_PROVIDER_EVENTS",
|
|
85
|
+
("emission", "style"): "MONKEYBOT_EMISSION_STYLE",
|
|
86
|
+
("runtime", "transcript_enabled"): "MONKEYBOT_TRANSCRIPT_ENABLED",
|
|
87
|
+
("runtime", "transcript_include_live"): "MONKEYBOT_TRANSCRIPT_INCLUDE_LIVE",
|
|
88
|
+
("harness", "mode"): "MONKEYBOT_HARNESS_MODE",
|
|
89
|
+
("realtime", "websocket.enabled"): "MONKEYBOT_REALTIME_WS_ENABLED",
|
|
90
|
+
("realtime", "websocket.port"): "MONKEYBOT_REALTIME_WS_PORT",
|
|
91
|
+
("realtime", "audio.input_format"): "MONKEYBOT_REALTIME_AUDIO_INPUT_FORMAT",
|
|
92
|
+
("realtime", "audio.output_format"): "MONKEYBOT_REALTIME_AUDIO_OUTPUT_FORMAT",
|
|
93
|
+
("realtime", "audio.chunk_ms"): "MONKEYBOT_REALTIME_AUDIO_CHUNK_MS",
|
|
94
|
+
("realtime", "audio.max_utterance_sec"): "MONKEYBOT_REALTIME_AUDIO_MAX_UTTERANCE_SEC",
|
|
95
|
+
("realtime", "session.max_duration_sec"): "MONKEYBOT_REALTIME_SESSION_MAX_DURATION_SEC",
|
|
96
|
+
("realtime", "session.idle_timeout_sec"): "MONKEYBOT_REALTIME_SESSION_IDLE_TIMEOUT_SEC",
|
|
97
|
+
("realtime", "session.max_response_turn_sec"): "MONKEYBOT_REALTIME_SESSION_MAX_RESPONSE_TURN_SEC",
|
|
98
|
+
("realtime", "session.max_concurrent_sessions"): "MONKEYBOT_REALTIME_SESSION_MAX_CONCURRENT_SESSIONS",
|
|
99
|
+
("realtime", "metrics.emit_summary_on_close"): "MONKEYBOT_REALTIME_METRICS_EMIT_SUMMARY_ON_CLOSE",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Backward-compatible alias for internal/tests.
|
|
103
|
+
_ENV_MAP = ENV_MAP
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def reset_runtime_env_state_for_tests() -> None:
|
|
107
|
+
"""Allow a second ``apply_monkeybot_runtime_env`` in the same interpreter (tests only)."""
|
|
108
|
+
global _RUNTIME_ENV_APPLIED
|
|
109
|
+
_RUNTIME_ENV_APPLIED = False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
out: dict[str, Any] = dict(base)
|
|
114
|
+
for k, v in overlay.items():
|
|
115
|
+
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
|
|
116
|
+
out[k] = _deep_merge(out[k], v)
|
|
117
|
+
else:
|
|
118
|
+
out[k] = v
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _scalar_to_env(value: Any) -> str | None:
|
|
123
|
+
if value is None:
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(value, bool):
|
|
126
|
+
return "true" if value else "false"
|
|
127
|
+
if isinstance(value, (int, float)):
|
|
128
|
+
return str(value)
|
|
129
|
+
if isinstance(value, str):
|
|
130
|
+
return value
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _denied_patterns_to_env(value: Any) -> str | None:
|
|
135
|
+
if value is None:
|
|
136
|
+
return None
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
return value
|
|
139
|
+
if isinstance(value, list):
|
|
140
|
+
parts = [str(x).strip() for x in value if str(x).strip()]
|
|
141
|
+
return ",".join(parts)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _get_nested(mapping: Mapping[str, Any], dotted_key: str) -> Any:
|
|
146
|
+
current: Any = mapping
|
|
147
|
+
for part in dotted_key.split("."):
|
|
148
|
+
if not isinstance(current, dict):
|
|
149
|
+
return None
|
|
150
|
+
current = current.get(part)
|
|
151
|
+
if current is None:
|
|
152
|
+
return None
|
|
153
|
+
return current
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _flatten_config(data: Mapping[str, Any]) -> dict[str, str]:
|
|
157
|
+
out: dict[str, str] = {}
|
|
158
|
+
for (section, key), env_name in ENV_MAP.items():
|
|
159
|
+
sec = data.get(section)
|
|
160
|
+
if not isinstance(sec, dict):
|
|
161
|
+
continue
|
|
162
|
+
raw = _get_nested(sec, key)
|
|
163
|
+
if raw is None:
|
|
164
|
+
continue
|
|
165
|
+
if env_name == "MONKEYBOT_TOOL_DENIED_PATTERNS":
|
|
166
|
+
s = _denied_patterns_to_env(raw)
|
|
167
|
+
else:
|
|
168
|
+
s = _scalar_to_env(raw)
|
|
169
|
+
if s is not None:
|
|
170
|
+
out[env_name] = s
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _resolve_config_path() -> Path | None:
|
|
175
|
+
explicit = os.environ.get("MONKEYBOT_CONFIG", "").strip()
|
|
176
|
+
if explicit:
|
|
177
|
+
p = Path(explicit).expanduser()
|
|
178
|
+
if p.is_file():
|
|
179
|
+
return p.resolve()
|
|
180
|
+
logger.warning("MONKEYBOT_CONFIG is set but not a file: %s", p)
|
|
181
|
+
return None
|
|
182
|
+
default = Path.cwd() / "monkeybot_config" / "monkeybot.yaml"
|
|
183
|
+
if default.is_file():
|
|
184
|
+
return default.resolve()
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _load_yaml_file(path: Path) -> dict[str, Any]:
|
|
189
|
+
with open(path, encoding="utf-8") as f:
|
|
190
|
+
loaded = yaml.safe_load(f)
|
|
191
|
+
if loaded is None:
|
|
192
|
+
return {}
|
|
193
|
+
if not isinstance(loaded, dict):
|
|
194
|
+
raise ValueError(f"Expected mapping at root of {path}, got {type(loaded).__name__}")
|
|
195
|
+
return loaded
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _merge_with_includes(primary_path: Path, root: dict[str, Any]) -> dict[str, Any]:
|
|
199
|
+
raw_includes = root.get("includes")
|
|
200
|
+
base = {k: v for k, v in root.items() if k != "includes"}
|
|
201
|
+
if not raw_includes:
|
|
202
|
+
return base
|
|
203
|
+
if not isinstance(raw_includes, list):
|
|
204
|
+
logger.warning("includes: must be a list of paths; ignoring")
|
|
205
|
+
return base
|
|
206
|
+
merged: dict[str, Any] = dict(base)
|
|
207
|
+
parent = primary_path.parent
|
|
208
|
+
for item in raw_includes:
|
|
209
|
+
if not isinstance(item, str) or not item.strip():
|
|
210
|
+
continue
|
|
211
|
+
inc_path = (parent / item.strip()).resolve()
|
|
212
|
+
if not inc_path.is_file():
|
|
213
|
+
logger.warning("include not found, skipping: %s", inc_path)
|
|
214
|
+
continue
|
|
215
|
+
try:
|
|
216
|
+
inc_doc = _load_yaml_file(inc_path)
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
logger.warning("failed to load include %s: %s", inc_path, exc)
|
|
219
|
+
continue
|
|
220
|
+
inc_body = {k: v for k, v in inc_doc.items() if k != "includes"}
|
|
221
|
+
merged = _deep_merge(merged, inc_body)
|
|
222
|
+
return merged
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def apply_monkeybot_runtime_env() -> Path | None:
|
|
226
|
+
"""Apply YAML-backed defaults to ``os.environ`` (unset keys only). Safe to call twice."""
|
|
227
|
+
global _RUNTIME_ENV_APPLIED
|
|
228
|
+
if _RUNTIME_ENV_APPLIED:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
path = _resolve_config_path()
|
|
232
|
+
if path is None:
|
|
233
|
+
_RUNTIME_ENV_APPLIED = True
|
|
234
|
+
logger.debug(
|
|
235
|
+
"No monkeybot.yaml found (MONKEYBOT_CONFIG / monkeybot_config/monkeybot.yaml); skipping"
|
|
236
|
+
)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
root = _load_yaml_file(path)
|
|
241
|
+
merged = _merge_with_includes(path, root)
|
|
242
|
+
flat = _flatten_config(merged)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.error("Failed to load %s: %s", path, exc)
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
_RUNTIME_ENV_APPLIED = True
|
|
248
|
+
google_cloud_project = (os.environ.get("GOOGLE_CLOUD_PROJECT") or "").strip()
|
|
249
|
+
for env_key, env_val in flat.items():
|
|
250
|
+
if env_key in os.environ:
|
|
251
|
+
continue
|
|
252
|
+
if env_key in _GCP_PROJECT_ENV_KEYS and google_cloud_project:
|
|
253
|
+
os.environ[env_key] = google_cloud_project
|
|
254
|
+
logger.debug(
|
|
255
|
+
"Set from GOOGLE_CLOUD_PROJECT: %s=%s", env_key, google_cloud_project
|
|
256
|
+
)
|
|
257
|
+
continue
|
|
258
|
+
os.environ[env_key] = env_val
|
|
259
|
+
logger.debug("Set from monkeybot.yaml: %s=%s", env_key, env_val)
|
|
260
|
+
|
|
261
|
+
logger.info("Applied runtime config from %s (%d keys)", path, len(flat))
|
|
262
|
+
return path
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Provider resolution and configuration types for the monkeybot harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
|
|
11
|
+
from monkeybot.core.llm.provider import Provider
|
|
12
|
+
from monkeybot.providers.claude import ClaudeProvider
|
|
13
|
+
from monkeybot.providers.gemini import GeminiProvider
|
|
14
|
+
from monkeybot.providers.huggingface import HuggingFaceProvider
|
|
15
|
+
from monkeybot.providers.nvidia import NvidiaProvider
|
|
16
|
+
from monkeybot.providers.ollama import OllamaProvider
|
|
17
|
+
from monkeybot.providers.openai import OpenAIProvider
|
|
18
|
+
from monkeybot.providers.sampling import resolve_model_sampling
|
|
19
|
+
from monkeybot.providers.vertex_claude import VertexClaudeProvider
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_MODEL_PROVIDER_ALIASES: dict[str, str] = {
|
|
24
|
+
"gemini": "google_vertexai",
|
|
25
|
+
"vertex": "google_vertexai",
|
|
26
|
+
"google-vertexai": "google_vertexai",
|
|
27
|
+
"vertex-claude": "vertex_anthropic",
|
|
28
|
+
"vertex_claude": "vertex_anthropic",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_model_provider(provider: str) -> str:
|
|
33
|
+
"""Map config aliases (e.g. ``gemini``, ``vertex-claude``) to canonical provider ids."""
|
|
34
|
+
key = provider.strip().lower()
|
|
35
|
+
return _MODEL_PROVIDER_ALIASES.get(key, key)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConfigError(Exception):
|
|
39
|
+
"""Configuration error with actionable message."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CustomMemoryFolder:
|
|
46
|
+
"""User-defined memory folder registered with the memory organizer classifier."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
description: str
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
import re
|
|
53
|
+
|
|
54
|
+
if not re.match(r"^[a-z0-9-]+$", self.name):
|
|
55
|
+
raise ConfigError(
|
|
56
|
+
f"CustomMemoryFolder.name '{self.name}' is invalid. "
|
|
57
|
+
"Use lowercase letters, digits, and hyphens only."
|
|
58
|
+
)
|
|
59
|
+
reserved = {"episodic", "semantic", "procedural", "working", "raw"}
|
|
60
|
+
if self.name in reserved:
|
|
61
|
+
raise ConfigError(
|
|
62
|
+
f"CustomMemoryFolder.name '{self.name}' conflicts with a built-in folder. "
|
|
63
|
+
f"Reserved names: {sorted(reserved)}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class SubagentConfig:
|
|
69
|
+
"""Configuration for a single named subagent persona."""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
description: str
|
|
73
|
+
skills: list[str]
|
|
74
|
+
agent_md: str | None = None
|
|
75
|
+
model: str | None = None
|
|
76
|
+
vertex_location: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ProviderConfig:
|
|
81
|
+
"""Native streaming Provider plus model id."""
|
|
82
|
+
|
|
83
|
+
provider: Provider
|
|
84
|
+
model: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve_gcp_project_id() -> str:
|
|
88
|
+
"""GCP project for Vertex providers."""
|
|
89
|
+
return (
|
|
90
|
+
(os.getenv("GCP_PROJECT_ID") or "").strip()
|
|
91
|
+
or (os.getenv("VERTEX_AI_PROJECT_ID") or "").strip()
|
|
92
|
+
or (os.getenv("ANTHROPIC_VERTEX_PROJECT_ID") or "").strip()
|
|
93
|
+
or (os.getenv("GOOGLE_CLOUD_PROJECT") or "").strip()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_provider_config(
|
|
98
|
+
provider: str | None = None,
|
|
99
|
+
model_name: str | None = None,
|
|
100
|
+
temperature: float | None = None,
|
|
101
|
+
max_tokens: int | None = None,
|
|
102
|
+
thinking_budget: int | None = None,
|
|
103
|
+
) -> ProviderConfig:
|
|
104
|
+
"""Resolve a Provider and model id from environment or explicit parameters."""
|
|
105
|
+
raw_provider = str(provider or os.getenv("MODEL_PROVIDER") or "google_vertexai")
|
|
106
|
+
provider_key = normalize_model_provider(raw_provider)
|
|
107
|
+
if provider_key == "fake":
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"MODEL_PROVIDER=fake is for gateway/tests only; inject ScriptedFakeProvider directly "
|
|
110
|
+
"or use the gateway fake provider path."
|
|
111
|
+
)
|
|
112
|
+
resolved_model = str(model_name or os.getenv("MODEL_NAME") or "gemini-2.5-flash")
|
|
113
|
+
sampling = resolve_model_sampling(temperature=temperature, max_tokens=max_tokens)
|
|
114
|
+
thinking_budget = (
|
|
115
|
+
thinking_budget if thinking_budget is not None else int(os.getenv("MODEL_THINKING_BUDGET", "-1"))
|
|
116
|
+
)
|
|
117
|
+
if provider_key == "google_vertexai":
|
|
118
|
+
return ProviderConfig(
|
|
119
|
+
GeminiProvider(
|
|
120
|
+
temperature=sampling.temperature,
|
|
121
|
+
max_tokens=sampling.max_tokens,
|
|
122
|
+
thinking_budget=thinking_budget,
|
|
123
|
+
),
|
|
124
|
+
resolved_model,
|
|
125
|
+
)
|
|
126
|
+
if provider_key == "google_genai":
|
|
127
|
+
api_key = (os.getenv("GEMINI_API_KEY") or "").strip()
|
|
128
|
+
if not api_key:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"MODEL_PROVIDER=google_genai requires GEMINI_API_KEY. "
|
|
131
|
+
"Set it in your environment or .env file."
|
|
132
|
+
)
|
|
133
|
+
return ProviderConfig(
|
|
134
|
+
GeminiProvider(
|
|
135
|
+
temperature=sampling.temperature,
|
|
136
|
+
max_tokens=sampling.max_tokens,
|
|
137
|
+
thinking_budget=thinking_budget,
|
|
138
|
+
api_key=api_key,
|
|
139
|
+
),
|
|
140
|
+
resolved_model,
|
|
141
|
+
)
|
|
142
|
+
if provider_key == "openai":
|
|
143
|
+
return ProviderConfig(
|
|
144
|
+
OpenAIProvider(
|
|
145
|
+
temperature=sampling.temperature,
|
|
146
|
+
max_tokens=sampling.max_tokens,
|
|
147
|
+
),
|
|
148
|
+
resolved_model,
|
|
149
|
+
)
|
|
150
|
+
if provider_key == "anthropic":
|
|
151
|
+
return ProviderConfig(
|
|
152
|
+
ClaudeProvider(
|
|
153
|
+
temperature=sampling.temperature,
|
|
154
|
+
max_tokens=sampling.max_tokens,
|
|
155
|
+
),
|
|
156
|
+
resolved_model,
|
|
157
|
+
)
|
|
158
|
+
if provider_key == "vertex_anthropic":
|
|
159
|
+
project = _resolve_gcp_project_id()
|
|
160
|
+
if not project:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"vertex_anthropic provider requires a GCP project. "
|
|
163
|
+
"Set GCP_PROJECT_ID, VERTEX_AI_PROJECT_ID, ANTHROPIC_VERTEX_PROJECT_ID, "
|
|
164
|
+
"or GOOGLE_CLOUD_PROJECT (or gcp.project_id in monkeybot.yaml)."
|
|
165
|
+
)
|
|
166
|
+
if os.getenv("VERTEX_AI_LOCATION"):
|
|
167
|
+
logger.warning(
|
|
168
|
+
"VERTEX_AI_LOCATION is no longer read for vertex_anthropic; "
|
|
169
|
+
"set ANTHROPIC_VERTEX_REGION instead"
|
|
170
|
+
)
|
|
171
|
+
region = (os.getenv("ANTHROPIC_VERTEX_REGION") or "us-east5").strip() or "us-east5"
|
|
172
|
+
return ProviderConfig(
|
|
173
|
+
VertexClaudeProvider(
|
|
174
|
+
project_id=project,
|
|
175
|
+
region=region,
|
|
176
|
+
temperature=sampling.temperature,
|
|
177
|
+
max_tokens=sampling.max_tokens,
|
|
178
|
+
),
|
|
179
|
+
resolved_model,
|
|
180
|
+
)
|
|
181
|
+
if provider_key == "huggingface":
|
|
182
|
+
return ProviderConfig(
|
|
183
|
+
HuggingFaceProvider(
|
|
184
|
+
temperature=sampling.temperature,
|
|
185
|
+
max_tokens=sampling.max_tokens,
|
|
186
|
+
),
|
|
187
|
+
resolved_model,
|
|
188
|
+
)
|
|
189
|
+
if provider_key == "ollama":
|
|
190
|
+
return ProviderConfig(
|
|
191
|
+
OllamaProvider(
|
|
192
|
+
temperature=sampling.temperature,
|
|
193
|
+
max_tokens=sampling.max_tokens,
|
|
194
|
+
thinking_budget=thinking_budget,
|
|
195
|
+
),
|
|
196
|
+
resolved_model,
|
|
197
|
+
)
|
|
198
|
+
if provider_key == "nvidia":
|
|
199
|
+
return ProviderConfig(
|
|
200
|
+
NvidiaProvider(
|
|
201
|
+
temperature=sampling.temperature,
|
|
202
|
+
max_tokens=sampling.max_tokens,
|
|
203
|
+
),
|
|
204
|
+
resolved_model,
|
|
205
|
+
)
|
|
206
|
+
if provider_key == "aws_bedrock":
|
|
207
|
+
from monkeybot.providers.bedrock import BedrockClaudeProvider # noqa: PLC0415
|
|
208
|
+
|
|
209
|
+
aws_region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION")
|
|
210
|
+
return ProviderConfig(
|
|
211
|
+
BedrockClaudeProvider(
|
|
212
|
+
aws_region=aws_region,
|
|
213
|
+
temperature=sampling.temperature,
|
|
214
|
+
max_tokens=sampling.max_tokens,
|
|
215
|
+
),
|
|
216
|
+
resolved_model,
|
|
217
|
+
)
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Unsupported model provider: {provider_key}. "
|
|
220
|
+
"Supported providers: google_vertexai, openai, anthropic, vertex_anthropic, "
|
|
221
|
+
"huggingface, ollama, nvidia, aws_bedrock"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _parse_subagent_entries(raw_entries: Any) -> list[SubagentConfig]:
|
|
226
|
+
if not raw_entries or not isinstance(raw_entries, list):
|
|
227
|
+
return []
|
|
228
|
+
configs: list[SubagentConfig] = []
|
|
229
|
+
for entry in raw_entries:
|
|
230
|
+
if not isinstance(entry, dict):
|
|
231
|
+
logger.warning("Skipping non-dict subagent entry: %s", entry)
|
|
232
|
+
continue
|
|
233
|
+
if "name" not in entry or "description" not in entry:
|
|
234
|
+
logger.warning("Skipping subagent entry missing 'name' or 'description': %s", entry)
|
|
235
|
+
continue
|
|
236
|
+
vloc = entry.get("vertex_location")
|
|
237
|
+
if vloc is None and "location" in entry:
|
|
238
|
+
vloc = entry.get("location")
|
|
239
|
+
raw_skills = entry.get("skills", [])
|
|
240
|
+
skills = list(raw_skills) if isinstance(raw_skills, list) else []
|
|
241
|
+
agent_md = entry.get("agent_md") or entry.get("prompt_file")
|
|
242
|
+
if agent_md is not None and not isinstance(agent_md, str):
|
|
243
|
+
logger.warning("Skipping subagent %s: agent_md must be a string", entry.get("name"))
|
|
244
|
+
continue
|
|
245
|
+
configs.append(
|
|
246
|
+
SubagentConfig(
|
|
247
|
+
name=entry["name"],
|
|
248
|
+
description=entry["description"],
|
|
249
|
+
skills=skills,
|
|
250
|
+
agent_md=agent_md.strip() if isinstance(agent_md, str) and agent_md.strip() else None,
|
|
251
|
+
model=entry.get("model"),
|
|
252
|
+
vertex_location=vloc,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
return configs
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_subagent_configs(config_path: str | None = None) -> list[SubagentConfig]:
|
|
259
|
+
"""Return subagent configurations from ``subagents:`` in monkeybot.yaml."""
|
|
260
|
+
_, doc = load_monkeybot_yaml_dict(config_path)
|
|
261
|
+
return _parse_subagent_entries(doc.get("subagents"))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_subagent_registry(config_path: str | None = None) -> dict[str, SubagentConfig]:
|
|
265
|
+
"""Named subagent personas keyed by ``name``; raises :class:`ConfigError` on duplicates."""
|
|
266
|
+
registry: dict[str, SubagentConfig] = {}
|
|
267
|
+
for cfg in get_subagent_configs(config_path):
|
|
268
|
+
if cfg.name in registry:
|
|
269
|
+
raise ConfigError(f"Duplicate subagent name in monkeybot.yaml: {cfg.name!r}")
|
|
270
|
+
registry[cfg.name] = cfg
|
|
271
|
+
return registry
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _bool_config_flag(
|
|
275
|
+
doc: dict[str, Any],
|
|
276
|
+
section: str,
|
|
277
|
+
key: str,
|
|
278
|
+
*,
|
|
279
|
+
default: bool,
|
|
280
|
+
label: str,
|
|
281
|
+
) -> bool:
|
|
282
|
+
section_obj = doc.get(section)
|
|
283
|
+
if not isinstance(section_obj, dict):
|
|
284
|
+
return default
|
|
285
|
+
raw = section_obj.get(key)
|
|
286
|
+
if raw is None:
|
|
287
|
+
return default
|
|
288
|
+
if isinstance(raw, bool):
|
|
289
|
+
return raw
|
|
290
|
+
raise ConfigError(f"{label} must be true or false, got {raw!r}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def auto_schema_enabled_from_config(config_path: str | None = None) -> bool:
|
|
294
|
+
"""Whether storage backends should apply DDL on ``open()`` (``paths.auto_schema``).
|
|
295
|
+
|
|
296
|
+
Defaults to ``True`` when the key is absent. Only read from monkeybot.yaml —
|
|
297
|
+
not from environment variables.
|
|
298
|
+
"""
|
|
299
|
+
_, doc = load_monkeybot_yaml_dict(config_path)
|
|
300
|
+
paths = doc.get("paths")
|
|
301
|
+
if not isinstance(paths, dict):
|
|
302
|
+
return True
|
|
303
|
+
raw = paths.get("auto_schema")
|
|
304
|
+
if raw is None:
|
|
305
|
+
return True
|
|
306
|
+
if isinstance(raw, bool):
|
|
307
|
+
return raw
|
|
308
|
+
raise ConfigError(f"paths.auto_schema must be true or false, got {raw!r}")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def vertex_google_search_enabled_from_config(config_path: str | None = None) -> bool:
|
|
312
|
+
"""Whether the main agent enables Gemini's native ``google_search`` grounding tool.
|
|
313
|
+
|
|
314
|
+
Read from ``web_search.vertex_google_search`` in monkeybot.yaml. Additive to, and
|
|
315
|
+
independent of, ``web_search.backend`` (the harness's pluggable DuckDuckGo/Tavily/
|
|
316
|
+
Firecrawl custom tool). Defaults to ``False`` when absent. Config-file only.
|
|
317
|
+
"""
|
|
318
|
+
_, doc = load_monkeybot_yaml_dict(config_path)
|
|
319
|
+
return _bool_config_flag(
|
|
320
|
+
doc,
|
|
321
|
+
"web_search",
|
|
322
|
+
"vertex_google_search",
|
|
323
|
+
default=False,
|
|
324
|
+
label="web_search.vertex_google_search",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def subagent_vertex_google_search_from_config(config_path: str | None = None) -> bool:
|
|
329
|
+
"""Whether subagent runs enable Gemini's native ``google_search`` grounding tool.
|
|
330
|
+
|
|
331
|
+
Read from ``subagent.vertex_google_search`` in monkeybot.yaml. Defaults to ``False``
|
|
332
|
+
when absent. Config-file only — not exposed via environment variables.
|
|
333
|
+
"""
|
|
334
|
+
_, doc = load_monkeybot_yaml_dict(config_path)
|
|
335
|
+
return _bool_config_flag(
|
|
336
|
+
doc,
|
|
337
|
+
"subagent",
|
|
338
|
+
"vertex_google_search",
|
|
339
|
+
default=False,
|
|
340
|
+
label="subagent.vertex_google_search",
|
|
341
|
+
)
|