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,82 @@
|
|
|
1
|
+
"""Pluggable web search for monkeybot.
|
|
2
|
+
|
|
3
|
+
Usage
|
|
4
|
+
-----
|
|
5
|
+
Call :func:`build_backend` once at startup (reads env vars). Pass the
|
|
6
|
+
returned backend to :class:`~monkeybot.core.tools.core_tool_executor.CoreToolExecutor`
|
|
7
|
+
via ``extra_tools``. ``None`` means web search is disabled.
|
|
8
|
+
|
|
9
|
+
Bring-your-own backend
|
|
10
|
+
-----------------------
|
|
11
|
+
Implement :class:`~monkeybot.web_search.protocol.WebSearchBackend` and wrap it
|
|
12
|
+
in a :class:`~monkeybot.core.context.CustomTool`::
|
|
13
|
+
|
|
14
|
+
class MyBackend:
|
|
15
|
+
name = "my_search"
|
|
16
|
+
async def search(self, query, *, max_results=5): ...
|
|
17
|
+
|
|
18
|
+
tool = web_search_custom_tool(MyBackend())
|
|
19
|
+
executor = CoreToolExecutor(..., extra_tools=[tool])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
from monkeybot.web_search.backends.duckduckgo import DuckDuckGoBackend
|
|
27
|
+
from monkeybot.web_search.backends.firecrawl import FirecrawlBackend
|
|
28
|
+
from monkeybot.web_search.backends.tavily import TavilyBackend
|
|
29
|
+
from monkeybot.web_search.protocol import SearchResult, WebSearchBackend
|
|
30
|
+
from monkeybot.web_search.tool import WebSearchTool
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SearchResult",
|
|
34
|
+
"WebSearchBackend",
|
|
35
|
+
"DuckDuckGoBackend",
|
|
36
|
+
"TavilyBackend",
|
|
37
|
+
"FirecrawlBackend",
|
|
38
|
+
"WebSearchTool",
|
|
39
|
+
"build_backend",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_backend() -> WebSearchBackend | None:
|
|
44
|
+
"""Construct the configured backend from environment variables.
|
|
45
|
+
|
|
46
|
+
``WEB_SEARCH_BACKEND`` selects the implementation:
|
|
47
|
+
|
|
48
|
+
* ``duckduckgo`` (default) — no API key required; uses the ``ddgs`` package.
|
|
49
|
+
* ``tavily`` — requires ``TAVILY_API_KEY``.
|
|
50
|
+
* ``firecrawl`` — requires ``FIRECRAWL_API_KEY``.
|
|
51
|
+
* ``none`` — disables web search entirely.
|
|
52
|
+
|
|
53
|
+
Returns ``None`` when ``WEB_SEARCH_BACKEND=none``.
|
|
54
|
+
"""
|
|
55
|
+
backend_name = os.environ.get("WEB_SEARCH_BACKEND", "duckduckgo").lower().strip()
|
|
56
|
+
|
|
57
|
+
if backend_name == "none":
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if backend_name == "tavily":
|
|
61
|
+
api_key = os.environ.get("TAVILY_API_KEY", "").strip()
|
|
62
|
+
if not api_key:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"WEB_SEARCH_BACKEND=tavily requires TAVILY_API_KEY to be set."
|
|
65
|
+
)
|
|
66
|
+
return TavilyBackend(api_key=api_key)
|
|
67
|
+
|
|
68
|
+
if backend_name == "firecrawl":
|
|
69
|
+
api_key = os.environ.get("FIRECRAWL_API_KEY", "").strip()
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"WEB_SEARCH_BACKEND=firecrawl requires FIRECRAWL_API_KEY to be set."
|
|
73
|
+
)
|
|
74
|
+
return FirecrawlBackend(api_key=api_key)
|
|
75
|
+
|
|
76
|
+
if backend_name == "duckduckgo":
|
|
77
|
+
return DuckDuckGoBackend()
|
|
78
|
+
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Unknown WEB_SEARCH_BACKEND={backend_name!r}. "
|
|
81
|
+
"Choose one of: duckduckgo, tavily, firecrawl, none."
|
|
82
|
+
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from monkeybot.web_search.backends.duckduckgo import DuckDuckGoBackend
|
|
2
|
+
from monkeybot.web_search.backends.firecrawl import FirecrawlBackend
|
|
3
|
+
from monkeybot.web_search.backends.tavily import TavilyBackend
|
|
4
|
+
|
|
5
|
+
__all__ = ["DuckDuckGoBackend", "FirecrawlBackend", "TavilyBackend"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""DuckDuckGo web search backend (zero-config default via the ``ddgs`` package)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from monkeybot.web_search.protocol import SearchResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DuckDuckGoBackend:
|
|
11
|
+
name = "duckduckgo"
|
|
12
|
+
|
|
13
|
+
async def search(self, query: str, *, max_results: int = 5) -> list[SearchResult]:
|
|
14
|
+
try:
|
|
15
|
+
from ddgs import DDGS
|
|
16
|
+
except ImportError as exc:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"ddgs is required for the DuckDuckGo backend. "
|
|
19
|
+
"Install with: uv add ddgs"
|
|
20
|
+
) from exc
|
|
21
|
+
|
|
22
|
+
raw = await asyncio.to_thread(
|
|
23
|
+
lambda: list(DDGS().text(query, max_results=max_results))
|
|
24
|
+
)
|
|
25
|
+
return [
|
|
26
|
+
SearchResult(
|
|
27
|
+
title=r.get("title", ""),
|
|
28
|
+
url=r.get("href", ""),
|
|
29
|
+
snippet=r.get("body", ""),
|
|
30
|
+
)
|
|
31
|
+
for r in raw
|
|
32
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Firecrawl web search backend (requires FIRECRAWL_API_KEY)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from monkeybot.web_search.protocol import SearchResult
|
|
6
|
+
|
|
7
|
+
_API_URL = "https://api.firecrawl.dev/v1/search"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FirecrawlBackend:
|
|
11
|
+
name = "firecrawl"
|
|
12
|
+
|
|
13
|
+
def __init__(self, *, api_key: str) -> None:
|
|
14
|
+
if not api_key:
|
|
15
|
+
raise ValueError("FIRECRAWL_API_KEY must be non-empty")
|
|
16
|
+
self._api_key = api_key
|
|
17
|
+
|
|
18
|
+
async def search(self, query: str, *, max_results: int = 5) -> list[SearchResult]:
|
|
19
|
+
try:
|
|
20
|
+
import httpx
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"httpx is required for the Firecrawl backend. "
|
|
24
|
+
"Install with: uv add httpx"
|
|
25
|
+
) from exc
|
|
26
|
+
|
|
27
|
+
payload = {"query": query, "limit": max_results}
|
|
28
|
+
headers = {"Authorization": f"Bearer {self._api_key}"}
|
|
29
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
30
|
+
resp = await client.post(_API_URL, json=payload, headers=headers)
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
data = resp.json()
|
|
33
|
+
|
|
34
|
+
results: list[SearchResult] = []
|
|
35
|
+
for r in data.get("data", []):
|
|
36
|
+
results.append(
|
|
37
|
+
SearchResult(
|
|
38
|
+
title=r.get("title", ""),
|
|
39
|
+
url=r.get("url", ""),
|
|
40
|
+
snippet=r.get("description", ""),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return results
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Tavily web search backend (requires TAVILY_API_KEY)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from monkeybot.web_search.protocol import SearchResult
|
|
6
|
+
|
|
7
|
+
_API_URL = "https://api.tavily.com/search"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TavilyBackend:
|
|
11
|
+
name = "tavily"
|
|
12
|
+
|
|
13
|
+
def __init__(self, *, api_key: str) -> None:
|
|
14
|
+
if not api_key:
|
|
15
|
+
raise ValueError("TAVILY_API_KEY must be non-empty")
|
|
16
|
+
self._api_key = api_key
|
|
17
|
+
|
|
18
|
+
async def search(self, query: str, *, max_results: int = 5) -> list[SearchResult]:
|
|
19
|
+
try:
|
|
20
|
+
import httpx
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"httpx is required for the Tavily backend. "
|
|
24
|
+
"Install with: uv add httpx"
|
|
25
|
+
) from exc
|
|
26
|
+
|
|
27
|
+
payload = {
|
|
28
|
+
"query": query,
|
|
29
|
+
"max_results": max_results,
|
|
30
|
+
"api_key": self._api_key,
|
|
31
|
+
}
|
|
32
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
33
|
+
resp = await client.post(_API_URL, json=payload)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
data = resp.json()
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
SearchResult(
|
|
39
|
+
title=r.get("title", ""),
|
|
40
|
+
url=r.get("url", ""),
|
|
41
|
+
snippet=r.get("content", ""),
|
|
42
|
+
score=r.get("score"),
|
|
43
|
+
)
|
|
44
|
+
for r in data.get("results", [])
|
|
45
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared types and protocol for pluggable web search backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SearchResult:
|
|
11
|
+
"""Normalised result returned by every backend."""
|
|
12
|
+
|
|
13
|
+
title: str
|
|
14
|
+
url: str
|
|
15
|
+
snippet: str
|
|
16
|
+
score: float | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class WebSearchBackend(Protocol):
|
|
21
|
+
"""Protocol every web search backend must satisfy."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
|
|
25
|
+
async def search(self, query: str, *, max_results: int = 5) -> list[SearchResult]: ...
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""CustomTool wrapper that exposes a WebSearchBackend as a harness tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from monkeybot.core.types.types_tools import ToolDef
|
|
9
|
+
from monkeybot.web_search.protocol import WebSearchBackend
|
|
10
|
+
|
|
11
|
+
_WEB_SEARCH_SCHEMA: dict[str, object] = {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"query": {"type": "string", "description": "The search query."},
|
|
15
|
+
"max_results": {
|
|
16
|
+
"type": "integer",
|
|
17
|
+
"description": "Maximum number of results to return (default 5).",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"required": ["query"],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WebSearchTool:
|
|
25
|
+
"""Adapts a :class:`WebSearchBackend` to the :class:`~monkeybot.core.context.CustomTool` protocol."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, backend: WebSearchBackend) -> None:
|
|
28
|
+
self._backend = backend
|
|
29
|
+
self.tool_def = ToolDef(
|
|
30
|
+
"web_search",
|
|
31
|
+
f"Search the web using {backend.name}. Returns titles, URLs, and text snippets.",
|
|
32
|
+
_WEB_SEARCH_SCHEMA,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def execute(self, args: dict[str, object]) -> str:
|
|
36
|
+
query = str(args.get("query") or "").strip()
|
|
37
|
+
if not query:
|
|
38
|
+
return json.dumps({"ok": False, "error": "web_search requires a non-empty query."})
|
|
39
|
+
|
|
40
|
+
default_max = int(os.environ.get("WEB_SEARCH_MAX_RESULTS", "5"))
|
|
41
|
+
raw_max = args.get("max_results")
|
|
42
|
+
if isinstance(raw_max, (int, float, str)):
|
|
43
|
+
try:
|
|
44
|
+
max_results = int(raw_max)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
max_results = default_max
|
|
47
|
+
else:
|
|
48
|
+
max_results = default_max
|
|
49
|
+
|
|
50
|
+
results = await self._backend.search(query, max_results=max_results)
|
|
51
|
+
items = [
|
|
52
|
+
{"title": r.title, "url": r.url, "snippet": r.snippet}
|
|
53
|
+
| ({"score": r.score} if r.score is not None else {})
|
|
54
|
+
for r in results
|
|
55
|
+
]
|
|
56
|
+
return json.dumps({"ok": True, "query": query, "results": items}, ensure_ascii=False)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monkeybot
|
|
3
|
+
Version: 2.1.1
|
|
4
|
+
Summary: monkeybot agent harness — FastAPI SSE gateway, owned loop, SQLite, and MCP.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
9
|
+
Requires-Dist: fastapi>=0.115.0
|
|
10
|
+
Requires-Dist: google-genai>=1.0.0
|
|
11
|
+
Requires-Dist: httpx>=0.27.0
|
|
12
|
+
Requires-Dist: mcp>=1.0.0
|
|
13
|
+
Requires-Dist: pydantic>=2.10.0
|
|
14
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
15
|
+
Requires-Dist: python-ulid>=2.0.0
|
|
16
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
18
|
+
Provides-Extra: aws
|
|
19
|
+
Requires-Dist: boto3>=1.34.0; extra == 'aws'
|
|
20
|
+
Provides-Extra: bedrock
|
|
21
|
+
Requires-Dist: anthropic[bedrock]>=0.40.0; extra == 'bedrock'
|
|
22
|
+
Requires-Dist: boto3>=1.34.0; extra == 'bedrock'
|
|
23
|
+
Provides-Extra: claude
|
|
24
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'claude'
|
|
25
|
+
Provides-Extra: cli
|
|
26
|
+
Requires-Dist: typer>=0.12.0; extra == 'cli'
|
|
27
|
+
Requires-Dist: websockets>=14.0; extra == 'cli'
|
|
28
|
+
Provides-Extra: cli-realtime
|
|
29
|
+
Requires-Dist: pyaudio>=0.2.11; extra == 'cli-realtime'
|
|
30
|
+
Requires-Dist: pynput>=1.7.6; extra == 'cli-realtime'
|
|
31
|
+
Requires-Dist: typer>=0.12.0; extra == 'cli-realtime'
|
|
32
|
+
Requires-Dist: websockets>=14.0; extra == 'cli-realtime'
|
|
33
|
+
Provides-Extra: council
|
|
34
|
+
Requires-Dist: google-cloud-storage>=2.18.0; extra == 'council'
|
|
35
|
+
Provides-Extra: evals
|
|
36
|
+
Requires-Dist: deepeval>=3.0.0; extra == 'evals'
|
|
37
|
+
Requires-Dist: langfuse>=3.0.0; extra == 'evals'
|
|
38
|
+
Provides-Extra: firestore
|
|
39
|
+
Requires-Dist: google-cloud-firestore>=2.16.0; extra == 'firestore'
|
|
40
|
+
Provides-Extra: gcs
|
|
41
|
+
Requires-Dist: google-cloud-storage>=2.18.0; extra == 'gcs'
|
|
42
|
+
Provides-Extra: gemini
|
|
43
|
+
Requires-Dist: google-genai>=1.0.0; extra == 'gemini'
|
|
44
|
+
Provides-Extra: huggingface
|
|
45
|
+
Requires-Dist: openai>=1.0; extra == 'huggingface'
|
|
46
|
+
Requires-Dist: tiktoken>=0.7.0; extra == 'huggingface'
|
|
47
|
+
Provides-Extra: nvidia
|
|
48
|
+
Requires-Dist: openai>=1.0; extra == 'nvidia'
|
|
49
|
+
Requires-Dist: tiktoken>=0.7.0; extra == 'nvidia'
|
|
50
|
+
Provides-Extra: observability
|
|
51
|
+
Requires-Dist: opentelemetry-api>=1.28; extra == 'observability'
|
|
52
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.28; extra == 'observability'
|
|
53
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.62b1; extra == 'observability'
|
|
54
|
+
Requires-Dist: opentelemetry-sdk>=1.28; extra == 'observability'
|
|
55
|
+
Provides-Extra: ollama
|
|
56
|
+
Requires-Dist: openai>=1.0; extra == 'ollama'
|
|
57
|
+
Requires-Dist: tiktoken>=0.7.0; extra == 'ollama'
|
|
58
|
+
Provides-Extra: openai
|
|
59
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
60
|
+
Requires-Dist: tiktoken>=0.7.0; extra == 'openai'
|
|
61
|
+
Provides-Extra: postgres
|
|
62
|
+
Requires-Dist: asyncpg>=0.29.0; extra == 'postgres'
|
|
63
|
+
Provides-Extra: realtime
|
|
64
|
+
Requires-Dist: websockets>=14.0; extra == 'realtime'
|
|
65
|
+
Provides-Extra: realtime-gemini
|
|
66
|
+
Requires-Dist: google-genai>=1.0.0; extra == 'realtime-gemini'
|
|
67
|
+
Requires-Dist: websockets>=14.0; extra == 'realtime-gemini'
|
|
68
|
+
Provides-Extra: sandbox
|
|
69
|
+
Requires-Dist: opensandbox>=0.1.7; extra == 'sandbox'
|
|
70
|
+
Provides-Extra: scheduler
|
|
71
|
+
Requires-Dist: croniter>=2.0; extra == 'scheduler'
|
|
72
|
+
Provides-Extra: vertex
|
|
73
|
+
Requires-Dist: google-api-python-client>=2.150.0; extra == 'vertex'
|
|
74
|
+
Requires-Dist: google-auth>=2.35.0; extra == 'vertex'
|
|
75
|
+
Requires-Dist: google-genai>=1.0.0; extra == 'vertex'
|
|
76
|
+
Provides-Extra: vertex-claude
|
|
77
|
+
Requires-Dist: anthropic[vertex]>=0.40.0; extra == 'vertex-claude'
|
|
78
|
+
Provides-Extra: web-search
|
|
79
|
+
Requires-Dist: ddgs>=7.0; extra == 'web-search'
|
|
80
|
+
Description-Content-Type: text/markdown
|
|
81
|
+
|
|
82
|
+
# monkeybot
|
|
83
|
+
|
|
84
|
+
<div align="center">
|
|
85
|
+
<img src="logo.png" alt="monkeybot Logo" width="600" />
|
|
86
|
+
|
|
87
|
+
<br />
|
|
88
|
+
|
|
89
|
+
*Multi-cloud agent harness — GCP-first docs, portable runtime*
|
|
90
|
+
|
|
91
|
+
[](LICENSE)
|
|
92
|
+
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
monkeybot is a thin **multi-cloud-capable** harness for tool-using LLM agents: FastAPI SSE gateway, pluggable storage (SQLite/Postgres/Firestore), MCP tools, skills, and memory. It runs locally with zero cloud dependencies and ships the same container image to any Docker host.
|
|
96
|
+
|
|
97
|
+
> [!TIP]
|
|
98
|
+
> **Positioning:** monkeybot is an owned agent runtime with a focused provider set — not a universal integration catalog. **Docs and examples are GCP-first** (Vertex, GCS, Cloud Run, Agent Engine); **AWS** paths (Bedrock, S3, AgentCore, ECS/Lambda) are shipped in the Pattern guides; Azure coverage is thinner. See [Cloud deployment — Positioning](docs/cloud-deployment-design.md#positioning).
|
|
99
|
+
|
|
100
|
+
> [!NOTE]
|
|
101
|
+
> Skills under `SKILLS_PATH` execute Python code; only add skills from trusted sources.
|
|
102
|
+
|
|
103
|
+
## Installation
|
|
104
|
+
|
|
105
|
+
Install [uv](https://docs.astral.sh/uv/), then the global CLI (no repo clone required):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # if needed
|
|
109
|
+
uv tool install monkeybot-cli
|
|
110
|
+
|
|
111
|
+
monkeybot new --dest ./my-agent --provider openai --yes
|
|
112
|
+
cd my-agent
|
|
113
|
+
uv sync
|
|
114
|
+
cp .env.example .env # add provider keys
|
|
115
|
+
monkeybot doctor
|
|
116
|
+
monkeybot chat
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`monkeybot new` writes an agent `pyproject.toml` with `monkeybot[<provider>]` (plus any `--with` extras). Plain `uv sync` installs the harness into the agent `.venv`. Upgrade the CLI with `uv tool upgrade monkeybot-cli`.
|
|
120
|
+
|
|
121
|
+
Full walkthrough (config, `.env`, MCP, Docker): **[Getting Started](docs/getting-started.md)**.
|
|
122
|
+
|
|
123
|
+
### Developing the harness (contributors)
|
|
124
|
+
|
|
125
|
+
Clone only if you are changing monkeybot itself:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/human-plus-machine/monkeybot.git
|
|
129
|
+
cd monkeybot && uv sync
|
|
130
|
+
cd cli && uv sync
|
|
131
|
+
uv tool install --editable .
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For a self-contained example agent that depends on this checkout via an editable path, see **[`demo_agent/`](demo_agent/)**.
|
|
135
|
+
|
|
136
|
+
## CLI (`monkeybot`)
|
|
137
|
+
|
|
138
|
+
The CLI scaffolds, validates, and talks to an agent from the terminal. Use `monkeybot chat` for the turn-based SSE gateway, or `monkeybot talk` for the realtime WebSocket gateway (audio/text).
|
|
139
|
+
|
|
140
|
+
### Talk to an agent
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cd path/to/agent # the dir containing monkeybot_config/monkeybot.yaml
|
|
144
|
+
monkeybot chat # spawns the gateway, connects, cleans up on exit
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Type a message and press Enter. `/bye` exits (and stops the gateway if this command started it). Ctrl-C also exits.
|
|
148
|
+
|
|
149
|
+
### Agent-first dependencies
|
|
150
|
+
|
|
151
|
+
The CLI is intentionally thin — it does **not** pull in provider/storage extras (`bedrock`, `postgres`, …) globally. Those extras are declared on the **agent project** (scaffolded `pyproject.toml` lists `monkeybot[…]`). `monkeybot run` and `monkeybot chat` spawn the gateway from the agent project's interpreter so the extras resolve correctly:
|
|
152
|
+
|
|
153
|
+
1. `<agent>/.venv/bin/python` — used directly when a project venv exists.
|
|
154
|
+
2. `uv run python -m monkeybot.gateway.main` — when `<agent>/pyproject.toml` exists but no `.venv`.
|
|
155
|
+
3. `sys.executable` (the CLI's interpreter) — legacy fallback for config-only trees (just `monkeybot_config/`, no `pyproject.toml`); in this case extras must be installed in the CLI env.
|
|
156
|
+
|
|
157
|
+
`monkeybot doctor` checks provider extras and Python version in that same interpreter, and its `remediation` field points at the agent project (add `monkeybot[<extra>]` to `pyproject.toml` dependencies, then `uv sync`), not the CLI.
|
|
158
|
+
|
|
159
|
+
To attach to a gateway you started yourself (e.g. to watch its logs), run it separately and connect with `--attach`:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
monkeybot run # terminal 1: gateway with live logs
|
|
163
|
+
monkeybot chat --attach # terminal 2: connect to the running gateway
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Commands
|
|
167
|
+
|
|
168
|
+
| Command | Purpose |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `monkeybot new` | Scaffold `monkeybot_config/`, workspace dirs, agent `pyproject.toml`, and `.env.example` |
|
|
171
|
+
| `monkeybot validate` | Check `monkeybot.yaml`, referenced paths, and MCP config (`--json` for machine output) |
|
|
172
|
+
| `monkeybot doctor` | Verify Python, provider extras, credentials, and port availability (`--json` for machine output) |
|
|
173
|
+
| `monkeybot run` | Start the SSE gateway in the foreground (keeps logs visible) |
|
|
174
|
+
| `monkeybot chat` | Talk to the agent (SSE); spawns the gateway by default (`--attach` to use a running one) |
|
|
175
|
+
| `monkeybot talk` | Realtime voice/text client (WebSocket); audio by default, `--text` for typed input |
|
|
176
|
+
|
|
177
|
+
Common flags: `--cwd` (agent root, defaults to the current directory), `--config` (explicit `monkeybot.yaml` path), `--port` / `--url` (override the config-derived gateway address). Secrets are read from the agent's `.env`; nothing is committed to `monkeybot.yaml`.
|
|
178
|
+
|
|
179
|
+
Realtime audio (`monkeybot talk` without `--text`) needs PortAudio plus `monkeybot[cli-realtime]` in the agent env (already included for `demo_agent`).
|
|
180
|
+
|
|
181
|
+
## Agent skill
|
|
182
|
+
|
|
183
|
+
Install the onboarding skill for Cursor and other agents via [skills.sh](https://skills.sh):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx skills add human-plus-machine/monkeybot --skill monkeybot
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The skill walks through CLI install, scaffolding `monkeybot_config/`, configuration, and your first chat. Source: [`cli/skills/monkeybot/`](cli/skills/monkeybot/).
|
|
190
|
+
|
|
191
|
+
## Requirements
|
|
192
|
+
|
|
193
|
+
Python 3.11+ · [uv](https://docs.astral.sh/uv/) · optional provider keys in `.env` (Gemini, OpenAI, Anthropic)
|
|
194
|
+
|
|
195
|
+
## Key capabilities
|
|
196
|
+
|
|
197
|
+
### Subagent task queue (optional)
|
|
198
|
+
|
|
199
|
+
When **`MONKEYBOT_TASK_QUEUE=1`**, the `task` tool enqueues subagent runs via `record_pending` instead of spawning inline. A storage backend (`DB_URL`) is **required** — queue mode without storage raises at enqueue time.
|
|
200
|
+
|
|
201
|
+
Run workers with `python -m monkeybot.subagents.worker` (production) or `MONKEYBOT_WORKER_POOL=1` on the gateway (development only). Worker tuning:
|
|
202
|
+
|
|
203
|
+
| Variable | Default | Purpose |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `MONKEYBOT_WORKER_STALE_CLAIM_MS` | `600000` (10 min) | Reclaim `running` rows with no heartbeat after this window; another worker may re-execute the run |
|
|
206
|
+
| `MONKEYBOT_WORKER_POLL_INTERVAL_S` | `2` | Poll interval for `pending_runs()` |
|
|
207
|
+
| `MONKEYBOT_WORKER_CONCURRENCY` | `1` | Max concurrent claimed runs per worker |
|
|
208
|
+
| `MONKEYBOT_WORKER_ID` | auto | Worker identity for claim attribution |
|
|
209
|
+
|
|
210
|
+
There is no claim heartbeat yet — subagent runs longer than `MONKEYBOT_WORKER_STALE_CLAIM_MS` risk duplicate execution. Increase the limit for long LLM workloads or keep runs under the window.
|
|
211
|
+
|
|
212
|
+
**Docker:** baseline production-style image — [`docker/Dockerfile`](docker/Dockerfile) + [`docker-compose.yml`](docker-compose.yml). Optional Cloud Run helpers may live in gitignored `internal/` for private forks; see **Step 3** in that doc.
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
**Config:** copy or scaffold **`monkeybot_config/monkeybot.yaml`** from **`monkeybot_config_example/monkeybot.example.yaml`** (or the packaged scaffold template). Secrets go in **`.env`** — see the YAML header for variable names.
|
|
216
|
+
|
|
217
|
+
## Documentation
|
|
218
|
+
|
|
219
|
+
| Guide | Description |
|
|
220
|
+
|---|---|
|
|
221
|
+
| [Getting Started](docs/getting-started.md) | Install, configure the gateway, and exercise sessions + SSE from the command line |
|
|
222
|
+
| [SSE gateway and custom UI](docs/sse-gateway-ui.md) | HTTP + SSE endpoints, event types, CORS/proxy notes, and how to wire your own frontend |
|
|
223
|
+
| [Skills](docs/skills.md) | Skill directory layout and `SKILL.md` discovery |
|
|
224
|
+
| [Model Context Protocol](docs/mcp.md) | MCP configuration, env interpolation, OAuth2 flows, and diagnostics |
|
|
225
|
+
| [Cloud deployment](docs/cloud-deployment-design.md) | Multi-cloud Patterns A/B/C — **GCP-first guides**, AWS addenda, portable harness |
|
|
226
|
+
|
|
227
|
+
## Integrations
|
|
228
|
+
|
|
229
|
+
Tables below reflect what **shipped in v2.0.0** (see [CHANGELOG](CHANGELOG.md)). Longer-term platform work lives in [BACKLOG.md](BACKLOG.md).
|
|
230
|
+
|
|
231
|
+
### LLM providers
|
|
232
|
+
|
|
233
|
+
| Provider | Status | Install extra |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| **Google Vertex AI / Gemini** | Supported | `monkeybot[gemini]` or ADC |
|
|
236
|
+
| **OpenAI** | Supported | `monkeybot[openai]` |
|
|
237
|
+
| **Anthropic Claude** | Supported | `monkeybot[claude]` |
|
|
238
|
+
| **Anthropic on Vertex AI** | Supported | `monkeybot[vertex-claude]` |
|
|
239
|
+
| **AWS Bedrock** | Supported | `monkeybot[bedrock]` |
|
|
240
|
+
| **HuggingFace Inference** | Supported | `monkeybot[huggingface]` |
|
|
241
|
+
| **Ollama** | Supported | `monkeybot[ollama]` |
|
|
242
|
+
| **NVIDIA NIM** (build.nvidia.com) | Supported | `monkeybot[nvidia]` |
|
|
243
|
+
|
|
244
|
+
### Storage & persistence
|
|
245
|
+
|
|
246
|
+
| Backend | Status | Install extra |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| **SQLite** | Default | — (SSE gateway session history) |
|
|
249
|
+
| **Postgres** | Supported | `monkeybot[postgres]` |
|
|
250
|
+
| **Google Cloud Firestore** | Supported | `monkeybot[firestore]` |
|
|
251
|
+
| **Local filesystem** | Default | `local://` memory URI |
|
|
252
|
+
| **Google Cloud Storage** | Supported | `monkeybot[gcs]` |
|
|
253
|
+
| **AWS S3** | Supported | `monkeybot[aws]` |
|
|
254
|
+
|
|
255
|
+
### Platform & tooling
|
|
256
|
+
|
|
257
|
+
| Integration | Status | Notes |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| **FastAPI SSE gateway** | Shipped | Sessions, streaming, health |
|
|
260
|
+
| **MCP** (stdio + HTTP) | Shipped | `monkeybot_config/mcp.json` |
|
|
261
|
+
| **Browser MCP** | Shipped | `integrations/browser-mcp/` |
|
|
262
|
+
| **OpenSandbox** | Shipped | Optional sandbox (`monkeybot[sandbox]`) |
|
|
263
|
+
| **Web search** | Shipped | DuckDuckGo (default, `monkeybot[web-search]`); Tavily / Firecrawl / Vertex grounding |
|
|
264
|
+
| **OpenTelemetry** | Shipped | `monkeybot[observability]` |
|
|
265
|
+
| **Docker / Cloud Run** | Shipped | Pattern A — [deploy guide](docs/deploy-pattern-a-container.md) |
|
|
266
|
+
| **Pattern C adapters** | Shipped | Agent platforms — e.g. AWS Bedrock AgentCore (`examples/agentcore/`), Vertex AI Agent Engine |
|
|
267
|
+
|
|
268
|
+
### Roadmap (near-term)
|
|
269
|
+
|
|
270
|
+
| Item | Notes |
|
|
271
|
+
|---|---|
|
|
272
|
+
| **Slack gateway** | Events API / socket mode — [BACKLOG](BACKLOG.md#1-chat-integration) |
|
|
273
|
+
| **Google Chat gateway** | Workspace webhook handler — [BACKLOG](BACKLOG.md#1-chat-integration) |
|
|
274
|
+
| **Harness evals** | Langfuse / deepeval traceability in the agent loop — in progress |
|
|
275
|
+
| **Scheduler in gateway** | Cron jobs for long-running deployments — design in [BACKLOG](BACKLOG.md#3-scheduler) |
|
|
276
|
+
|
|
277
|
+
Azure, Microsoft Teams, Telegram, DynamoDB, CosmosDB, and managed secret resolvers (AWS Secrets Manager, Azure Key Vault, full GCP Secret Manager wiring) are tracked under **Future platforms** in [BACKLOG.md](BACKLOG.md).
|
|
278
|
+
|
|
279
|
+
For provider and cloud wiring, start from **`monkeybot_config_example/monkeybot.example.yaml`** and the [Cloud deployment](docs/cloud-deployment-design.md) guide.
|
|
280
|
+
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
uv sync
|
|
285
|
+
uv run pytest
|
|
286
|
+
uv run ruff check .
|
|
287
|
+
uv run mypy src/
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Support
|
|
291
|
+
|
|
292
|
+
If you need help or hit a problem, search existing issues or open a new one in the [GitHub issue tracker](https://github.com/human-plus-machine/monkeybot/issues).
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
Contributions are welcome. You can help by:
|
|
297
|
+
|
|
298
|
+
- Reporting bugs or gaps in the docs (via issues).
|
|
299
|
+
- Suggesting new providers, interfaces, or deployment patterns (feature requests welcome).
|
|
300
|
+
- Opening pull requests for fixes and improvements.
|
|
301
|
+
|
|
302
|
+
Run checks before submitting: `uv run pytest && uv run ruff check . && uv run mypy src/`
|
|
303
|
+
|
|
304
|
+
### Releasing
|
|
305
|
+
|
|
306
|
+
`develop` is the working branch; `main` always reflects the latest release.
|
|
307
|
+
|
|
308
|
+
1. Anyone runs the **Prepare release** workflow (Actions tab → Prepare release → Run workflow), choosing `core` or `cli` and a version bump. It bumps the package's `pyproject.toml`, moves the `CHANGELOG.md` `Unreleased` section into a dated entry on a `release/<package>-v<version>` branch, and opens a PR from that branch into `main` (`develop` itself is untouched until the PR merges, so an abandoned release PR leaves no trace).
|
|
309
|
+
2. An admin reviews and **merges the PR** (branch protection on `main` restricts who can merge — see below). Use a regular merge commit, not squash, so `main`'s history and the release tag line up with what was reviewed.
|
|
310
|
+
3. The **Publish release** workflow runs automatically on that merge: it tags the new version, creates a GitHub Release from the changelog entry, Trusted-Publishes the released package(s) to PyPI (core before CLI when both ship), and merges `main` back into `develop` so the two branches don't drift apart.
|
|
311
|
+
|
|
312
|
+
One-time setup (repo Settings → Branches → add rule for `main`): require a pull request before merging, and restrict who can push to matching branches to Admins. That's the only access control needed — anyone can prepare a release, only admins can promote it to `main`.
|
|
313
|
+
|
|
314
|
+
**Before relying on this:** `develop` and `main` currently have diverged history (independent commits on each side). Reconcile them once with a manual merge before running the first automated release, or the first release PR may show unrelated changes or conflicts. `CHANGELOG.md` is shared across both packages — `core` and `cli` versions are bumped independently but their release notes live in the same file. The current versions already on `main` (`core` 2.0.0, `cli` 0.1.7) predate this tooling and have no changelog entry, so the first `publish` run intentionally skips tagging them rather than creating a release with empty notes; tagging starts from the next real version bump.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
You may use, modify, and distribute monkeybot under the MIT license. See the [`LICENSE`](LICENSE) file for details.
|