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.
Files changed (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,152 @@
1
+ """Ollama provider via the OpenAI-compatible Chat Completions API.
2
+
3
+ Runs against a local (or remote) Ollama server — no API key required. Ollama
4
+ exposes an OpenAI-compatible endpoint at ``/v1`` on top of its native API, so
5
+ this adapter reuses the same request/response plumbing as ``OpenAIProvider``
6
+ and ``HuggingFaceProvider``.
7
+
8
+ Configuration (environment variables or ``monkeybot.yaml``):
9
+
10
+ - ``OLLAMA_BASE_URL`` — OpenAI-compat base host (default: ``http://localhost:11434``;
11
+ ``/v1`` is appended when missing)
12
+ - ``OLLAMA_API_KEY`` — optional; Ollama ignores the value but the OpenAI SDK
13
+ requires a non-empty string. Set this only if a reverse proxy enforces auth.
14
+ - ``MODEL_NAME`` — model id passed to the API (e.g. ``llama3.1``, ``qwen2.5``).
15
+ Must already be pulled on the Ollama server (``ollama pull <model>``).
16
+ - ``MODEL_TEMPERATURE`` — sampling temperature (default: ``0.7``; set via ``monkeybot.yaml`` / constructor)
17
+ - ``MODEL_MAX_TOKENS`` — max output tokens (default: ``60000``; set via ``monkeybot.yaml`` / constructor)
18
+ - ``MODEL_THINKING_BUDGET`` — thinking control for reasoning models (e.g. Gemma 4):
19
+ ``-1`` = Ollama default (thinking on when supported), ``0`` = off via
20
+ ``reasoning_effort: none``, ``N > 0`` = on (no token budget on Ollama).
21
+
22
+ Install the required extra::
23
+
24
+ uv sync --extra ollama
25
+ # or: pip install "monkeybot[ollama]"
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from collections.abc import AsyncIterator, Sequence
32
+
33
+ from monkeybot.core.llm.provider import (
34
+ Message,
35
+ ProviderEvent,
36
+ )
37
+ from monkeybot.core.types.types_tools import ToolDef
38
+ from monkeybot.providers._openai_compat import (
39
+ count_input_tokens_tiktoken,
40
+ stream_chat_completions_with_tool_fallback,
41
+ )
42
+ from monkeybot.providers.sampling import resolve_model_sampling
43
+
44
+ _DEFAULT_BASE_URL = "http://localhost:11434"
45
+ _DUMMY_API_KEY = "ollama"
46
+
47
+
48
+ def reasoning_effort_for_thinking_budget(budget: int) -> str | None:
49
+ """Map monkeybot ``thinking_budget`` to Ollama OpenAI-compat ``reasoning_effort``.
50
+
51
+ Returns ``None`` when the field should be omitted (server default).
52
+ """
53
+ if budget == 0:
54
+ return "none"
55
+ return None
56
+
57
+
58
+ def _resolve_thinking_budget(
59
+ configured: int | None,
60
+ *,
61
+ override: int | None,
62
+ ) -> int:
63
+ if override is not None:
64
+ return override
65
+ if configured is not None:
66
+ return configured
67
+ return int(os.environ.get("MODEL_THINKING_BUDGET", "-1"))
68
+
69
+
70
+ class OllamaProvider:
71
+ """Local (or self-hosted) models via Ollama's OpenAI-compatible endpoint.
72
+
73
+ Requires ``monkeybot[ollama]`` (``openai`` + ``tiktoken``) and a reachable
74
+ Ollama server with the configured model already pulled. No API key is
75
+ required by default.
76
+ """
77
+
78
+ @property
79
+ def name(self) -> str:
80
+ return "ollama"
81
+
82
+ @property
83
+ def supports_streaming(self) -> bool:
84
+ return True
85
+
86
+ def __init__(
87
+ self,
88
+ *,
89
+ temperature: float | None = None,
90
+ max_tokens: int | None = None,
91
+ thinking_budget: int | None = None,
92
+ ) -> None:
93
+ self._base_url = (os.environ.get("OLLAMA_BASE_URL") or _DEFAULT_BASE_URL).rstrip("/")
94
+ self._api_key = os.environ.get("OLLAMA_API_KEY") or _DUMMY_API_KEY
95
+ sampling = resolve_model_sampling(temperature=temperature, max_tokens=max_tokens)
96
+ self._temperature = sampling.temperature
97
+ self._max_tokens = sampling.max_tokens
98
+ self._thinking_budget = (
99
+ thinking_budget
100
+ if thinking_budget is not None
101
+ else int(os.environ.get("MODEL_THINKING_BUDGET", "-1"))
102
+ )
103
+
104
+ def _resolve_base_url(self, model: str) -> str:
105
+ """Return the OpenAI-compat base URL for ``model``.
106
+
107
+ ``OLLAMA_BASE_URL`` is used as-is when it already ends in ``/v1``;
108
+ otherwise ``/v1`` is appended. Defaults to a local Ollama server.
109
+ """
110
+ del model # model id is passed per request; base URL is env-driven
111
+ host = self._base_url
112
+ if host.endswith("/v1"):
113
+ return host
114
+ return f"{host}/v1"
115
+
116
+ async def count_input_tokens(
117
+ self,
118
+ messages: Sequence[Message],
119
+ tools: Sequence[ToolDef],
120
+ *,
121
+ model: str,
122
+ thinking_budget: int | None = None,
123
+ ) -> int:
124
+ return await count_input_tokens_tiktoken(
125
+ messages, tools, model=model, thinking_budget=thinking_budget
126
+ )
127
+
128
+ async def stream(
129
+ self,
130
+ messages: Sequence[Message],
131
+ tools: Sequence[ToolDef],
132
+ *,
133
+ model: str,
134
+ thinking_budget: int | None = None,
135
+ ) -> AsyncIterator[ProviderEvent]:
136
+ budget = _resolve_thinking_budget(self._thinking_budget, override=thinking_budget)
137
+ reasoning_effort = reasoning_effort_for_thinking_budget(budget)
138
+ effort_kw = (
139
+ {"reasoning_effort": reasoning_effort} if reasoning_effort is not None else {}
140
+ )
141
+ async for event in stream_chat_completions_with_tool_fallback(
142
+ base_url=self._resolve_base_url(model),
143
+ api_key=self._api_key,
144
+ provider="ollama",
145
+ messages=messages,
146
+ tools=tools,
147
+ model=model,
148
+ temperature=self._temperature,
149
+ max_tokens=self._max_tokens,
150
+ **effort_kw,
151
+ ):
152
+ yield event
@@ -0,0 +1,127 @@
1
+ """OpenAI Chat Completions provider (direct ``openai`` SDK, async streaming)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import AsyncIterator, Sequence
7
+ from typing import Any
8
+
9
+ from monkeybot.core.llm.provider import (
10
+ Message,
11
+ ProviderCallHints,
12
+ ProviderEvent,
13
+ )
14
+ from monkeybot.core.types.types_tools import ToolDef
15
+ from monkeybot.providers._openai_compat import (
16
+ count_openai_compat_input_tokens,
17
+ iter_openai_compat_stream,
18
+ messages_to_openai,
19
+ openai_messages_token_count,
20
+ openai_tools,
21
+ openai_tools_token_count,
22
+ )
23
+ from monkeybot.providers.sampling import resolve_model_sampling
24
+
25
+ # Re-export private names that existing tests import directly from this module.
26
+ _messages_to_openai = messages_to_openai
27
+ _openai_tools = openai_tools
28
+ _openai_messages_token_count = openai_messages_token_count
29
+ _openai_tools_token_count = openai_tools_token_count
30
+
31
+ __all__ = [
32
+ "OpenAIProvider",
33
+ # legacy private names kept for tests
34
+ "_messages_to_openai",
35
+ "_openai_tools",
36
+ "_openai_messages_token_count",
37
+ "_openai_tools_token_count",
38
+ ]
39
+
40
+
41
+ class OpenAIProvider:
42
+ """OpenAI chat models using the official ``openai`` async client."""
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ return "openai"
47
+
48
+ @property
49
+ def supports_streaming(self) -> bool:
50
+ return True
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ temperature: float | None = None,
56
+ max_tokens: int | None = None,
57
+ ) -> None:
58
+ if not os.environ.get("OPENAI_API_KEY"):
59
+ raise ValueError("OPENAI_API_KEY is not set")
60
+ sampling = resolve_model_sampling(temperature=temperature, max_tokens=max_tokens)
61
+ self._temperature = sampling.temperature
62
+ self._max_tokens = sampling.max_tokens
63
+
64
+ async def count_input_tokens(
65
+ self,
66
+ messages: Sequence[Message],
67
+ tools: Sequence[ToolDef],
68
+ *,
69
+ model: str,
70
+ thinking_budget: int | None = None,
71
+ hints: ProviderCallHints | None = None,
72
+ ) -> int:
73
+ del thinking_budget, hints
74
+ import tiktoken # noqa: PLC0415
75
+
76
+ msgs = list(messages)
77
+ try:
78
+ enc = tiktoken.encoding_for_model(model)
79
+ except KeyError:
80
+ enc = tiktoken.get_encoding("cl100k_base")
81
+ return count_openai_compat_input_tokens(enc, msgs, tools)
82
+
83
+ async def stream(
84
+ self,
85
+ messages: Sequence[Message],
86
+ tools: Sequence[ToolDef],
87
+ *,
88
+ model: str,
89
+ thinking_budget: int | None = None,
90
+ hints: ProviderCallHints | None = None,
91
+ ) -> AsyncIterator[ProviderEvent]:
92
+ del thinking_budget
93
+ from openai import AsyncOpenAI # noqa: PLC0415
94
+
95
+ msgs = list(messages)
96
+
97
+ system, oai_messages = messages_to_openai(msgs)
98
+ if system:
99
+ oai_messages = [{"role": "system", "content": system}, *oai_messages]
100
+
101
+ retention = hints.cache_retention if hints is not None else "short"
102
+ session_id = hints.session_id if hints is not None else None
103
+ client_kwargs: dict[str, Any] = {}
104
+ if session_id and retention != "none":
105
+ client_kwargs["default_headers"] = {"x-session-affinity": session_id}
106
+ client = AsyncOpenAI(**client_kwargs)
107
+ kwargs: dict[str, Any] = {
108
+ "model": model,
109
+ "messages": oai_messages,
110
+ "stream": True,
111
+ "temperature": self._temperature,
112
+ "max_tokens": self._max_tokens,
113
+ }
114
+ if tools:
115
+ kwargs["tools"] = openai_tools(tools)
116
+ kwargs["parallel_" + "tool" + "_calls"] = True
117
+ if retention == "long":
118
+ kwargs["prompt_cache_retention"] = "24h"
119
+
120
+ async for event in iter_openai_compat_stream(
121
+ client,
122
+ kwargs,
123
+ provider="openai",
124
+ n_messages=len(messages),
125
+ n_tools=len(tools),
126
+ ):
127
+ yield event
@@ -0,0 +1,60 @@
1
+ """Model pricing lookup and cost estimation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ _log = logging.getLogger(__name__)
8
+
9
+ # USD per 1M tokens: (input, output, cache_write, cache_read)
10
+ # TODO: add Bedrock model entries — aws_bedrock users get cost_usd 0.0 until then.
11
+ # Gemini-on-Vertex uses the same gemini-* keys when the model id matches.
12
+ MODEL_PRICING: dict[str, tuple[float, float, float, float]] = {
13
+ "claude-sonnet-4": (3.00, 15.00, 3.75, 0.30),
14
+ "claude-haiku-4-5": (1.00, 5.00, 1.25, 0.10),
15
+ "gpt-5": (1.25, 10.00, 0.00, 0.125),
16
+ "gemini-2.5-flash": (0.30, 2.50, 0.00, 0.075),
17
+ "gemini-3-flash-preview": (0.30, 2.50, 0.00, 0.075),
18
+ }
19
+
20
+ __all__ = ["MODEL_PRICING", "estimate_cost", "pricing_for"]
21
+
22
+
23
+ def pricing_for(model: str) -> tuple[float, float, float, float]:
24
+ """Return (input, output, cache_write, cache_read) $/M for ``model``.
25
+
26
+ Exact match first, then longest known-prefix match, else zeros.
27
+ Never raises on unknown models.
28
+ """
29
+ exact = MODEL_PRICING.get(model)
30
+ if exact is not None:
31
+ return exact
32
+
33
+ best_key: str | None = None
34
+ for key in MODEL_PRICING:
35
+ if model.startswith(key) and (best_key is None or len(key) > len(best_key)):
36
+ best_key = key
37
+
38
+ if best_key is not None:
39
+ return MODEL_PRICING[best_key]
40
+
41
+ _log.warning("unknown model for pricing lookup: %s", model)
42
+ return (0.0, 0.0, 0.0, 0.0)
43
+
44
+
45
+ def estimate_cost(
46
+ model: str,
47
+ input_tokens: int,
48
+ output_tokens: int,
49
+ *,
50
+ cache_read_tokens: int = 0,
51
+ cache_creation_tokens: int = 0,
52
+ ) -> float:
53
+ """Return estimated USD cost using :func:`pricing_for`. Unknown model → 0.0."""
54
+ rin, rout, rcw, rcr = pricing_for(model)
55
+ return (
56
+ input_tokens * rin
57
+ + output_tokens * rout
58
+ + cache_creation_tokens * rcw
59
+ + cache_read_tokens * rcr
60
+ ) / 1_000_000
@@ -0,0 +1,44 @@
1
+ """Unified model sampling parameters (temperature, max output tokens).
2
+
3
+ All providers resolve sampling once at construction via :func:`resolve_model_sampling`.
4
+ :func:`~monkeybot.core.config.settings.get_provider_config` is the primary entry point;
5
+ direct provider construction falls back to ``MODEL_TEMPERATURE`` / ``MODEL_MAX_TOKENS``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from dataclasses import dataclass
12
+
13
+ DEFAULT_MODEL_TEMPERATURE = 0.7
14
+ DEFAULT_MODEL_MAX_TOKENS = 60_000
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ModelSamplingConfig:
19
+ """Resolved sampling knobs for a single provider instance."""
20
+
21
+ temperature: float
22
+ max_tokens: int
23
+
24
+
25
+ def resolve_model_sampling(
26
+ *,
27
+ temperature: float | None = None,
28
+ max_tokens: int | None = None,
29
+ ) -> ModelSamplingConfig:
30
+ """Resolve temperature and max output tokens from explicit args or environment."""
31
+ resolved_temperature = (
32
+ float(temperature)
33
+ if temperature is not None
34
+ else float(os.getenv("MODEL_TEMPERATURE", str(DEFAULT_MODEL_TEMPERATURE)))
35
+ )
36
+ resolved_max_tokens = (
37
+ int(max_tokens)
38
+ if max_tokens is not None
39
+ else int(os.getenv("MODEL_MAX_TOKENS", str(DEFAULT_MODEL_MAX_TOKENS)))
40
+ )
41
+ return ModelSamplingConfig(
42
+ temperature=resolved_temperature,
43
+ max_tokens=resolved_max_tokens,
44
+ )
@@ -0,0 +1,148 @@
1
+ """Anthropic Claude on Vertex AI (ADC)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from collections.abc import AsyncIterator, Sequence
8
+ from typing import Any, cast
9
+
10
+ from monkeybot.core.llm.provider import Message, ProviderCallHints, ProviderEvent
11
+ from monkeybot.core.logging_utils import kv
12
+ from monkeybot.core.types.types_tools import ToolDef
13
+ from monkeybot.providers._utils import (
14
+ anthropic_tool_defs,
15
+ build_anthropic_messages,
16
+ build_cached_system_blocks,
17
+ count_anthropic_input_tokens,
18
+ estimate_anthropic_input_tokens,
19
+ iter_anthropic_sdk_stream,
20
+ mark_last_tool_cached,
21
+ split_leading_system,
22
+ )
23
+ from monkeybot.providers.sampling import resolve_model_sampling
24
+
25
+ _log = logging.getLogger(__name__)
26
+
27
+
28
+ class VertexClaudeProvider:
29
+ """Claude on Vertex AI — ADC, no ``ANTHROPIC_API_KEY``."""
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return "vertex-claude"
34
+
35
+ @property
36
+ def supports_streaming(self) -> bool:
37
+ return True
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ project_id: str | None = None,
43
+ region: str | None = None,
44
+ temperature: float | None = None,
45
+ max_tokens: int | None = None,
46
+ ) -> None:
47
+ self._project_id = (
48
+ (project_id or "").strip()
49
+ or os.environ.get("ANTHROPIC_VERTEX_PROJECT_ID", "").strip()
50
+ )
51
+ self._region = (
52
+ (region or "").strip()
53
+ or os.environ.get("ANTHROPIC_VERTEX_REGION", "").strip()
54
+ or "global"
55
+ )
56
+ if not self._project_id:
57
+ raise ValueError("ANTHROPIC_VERTEX_PROJECT_ID is not set (or pass project_id=)")
58
+ sampling = resolve_model_sampling(temperature=temperature, max_tokens=max_tokens)
59
+ self._temperature = sampling.temperature
60
+ self._max_tokens = sampling.max_tokens
61
+
62
+ async def count_input_tokens(
63
+ self,
64
+ messages: Sequence[Message],
65
+ tools: Sequence[ToolDef],
66
+ *,
67
+ model: str,
68
+ thinking_budget: int | None = None,
69
+ hints: ProviderCallHints | None = None,
70
+ ) -> int:
71
+ del thinking_budget, hints
72
+ import anthropic # noqa: PLC0415
73
+ from anthropic import AsyncAnthropicVertex # noqa: PLC0415
74
+
75
+ system, msgs = split_leading_system(messages)
76
+ converted_messages = build_anthropic_messages(msgs)
77
+ client = AsyncAnthropicVertex(project_id=self._project_id, region=self._region)
78
+ converted_tools = anthropic_tool_defs(tools) if tools else None
79
+ try:
80
+ return await count_anthropic_input_tokens(
81
+ client,
82
+ anthropic_module=anthropic,
83
+ model=model,
84
+ system=system,
85
+ messages=cast(Any, converted_messages),
86
+ tools=cast(Any, converted_tools),
87
+ )
88
+ except Exception as exc:
89
+ msg = str(exc).lower()
90
+ if "token counting" in msg or "not supported" in msg:
91
+ _log.warning(
92
+ "Vertex Claude count_tokens unavailable, using estimate %s",
93
+ kv(provider="vertex_claude", model=model),
94
+ exc_info=True,
95
+ )
96
+ return estimate_anthropic_input_tokens(
97
+ system=system,
98
+ messages=converted_messages,
99
+ tools=converted_tools,
100
+ )
101
+ raise
102
+
103
+ async def stream(
104
+ self,
105
+ messages: Sequence[Message],
106
+ tools: Sequence[ToolDef],
107
+ *,
108
+ model: str,
109
+ thinking_budget: int | None = None,
110
+ hints: ProviderCallHints | None = None,
111
+ ) -> AsyncIterator[ProviderEvent]:
112
+ del thinking_budget
113
+ import anthropic # noqa: PLC0415
114
+ from anthropic import AsyncAnthropicVertex # noqa: PLC0415
115
+
116
+ retention = hints.cache_retention if hints is not None else "short"
117
+ system, msgs = split_leading_system(messages)
118
+ converted_messages = build_anthropic_messages(msgs)
119
+ client = AsyncAnthropicVertex(project_id=self._project_id, region=self._region)
120
+ converted = anthropic_tool_defs(tools) if tools else None
121
+ system_param: Any = (
122
+ build_cached_system_blocks(system, cache_retention=retention)
123
+ if system
124
+ else anthropic.NOT_GIVEN
125
+ )
126
+ tools_param: Any = (
127
+ mark_last_tool_cached(converted, cache_retention=retention)
128
+ if converted
129
+ else anthropic.NOT_GIVEN
130
+ )
131
+
132
+ stream_kwargs: dict[str, Any] = {
133
+ "model": model,
134
+ "system": system_param,
135
+ "messages": cast(Any, converted_messages),
136
+ "tools": tools_param,
137
+ "max_tokens": self._max_tokens,
138
+ "temperature": self._temperature,
139
+ }
140
+ async for event in iter_anthropic_sdk_stream(
141
+ client,
142
+ stream_kwargs,
143
+ provider="vertex_claude",
144
+ error_message="Vertex Claude stream error: %s",
145
+ n_messages=len(messages),
146
+ n_tools=len(tools),
147
+ ):
148
+ yield event
@@ -0,0 +1,33 @@
1
+ """Compatibility shim — scaffolding lives in ``monkeybot_cli.scaffold``.
2
+
3
+ Docker images and older imports still use ``monkeybot.scaffold.run_new``. Prefer
4
+ ``from monkeybot_cli.scaffold import run_new`` in new code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ try:
10
+ from monkeybot_cli.scaffold import ( # noqa: F401
11
+ ensure_memory,
12
+ ensure_workspace,
13
+ install_config_bundle,
14
+ install_env_example,
15
+ install_setup_script,
16
+ run_new,
17
+ write_active_config,
18
+ )
19
+ except ImportError as exc: # pragma: no cover - clearer error when CLI not installed
20
+ raise ImportError(
21
+ "Scaffolding moved to the monkeybot-cli package. "
22
+ "Install monkeybot-cli (e.g. `uv tool install monkeybot-cli` or `pip install ./cli`)."
23
+ ) from exc
24
+
25
+ __all__ = [
26
+ "ensure_memory",
27
+ "ensure_workspace",
28
+ "install_config_bundle",
29
+ "install_env_example",
30
+ "install_setup_script",
31
+ "run_new",
32
+ "write_active_config",
33
+ ]
@@ -0,0 +1,13 @@
1
+ """Scheduled-loop worker package."""
2
+
3
+ from monkeybot.scheduler.engine import (
4
+ scheduler_enabled_from_env,
5
+ scheduler_settings,
6
+ start_scheduler_background,
7
+ )
8
+
9
+ __all__ = [
10
+ "scheduler_enabled_from_env",
11
+ "scheduler_settings",
12
+ "start_scheduler_background",
13
+ ]
@@ -0,0 +1,4 @@
1
+ from monkeybot.scheduler.worker import main
2
+
3
+ if __name__ == "__main__":
4
+ main()