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,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
|
+
]
|