EvoScientist 0.0.1b0__tar.gz
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.
- evoscientist-0.0.1b0/EvoScientist/EvoScientist.py +336 -0
- evoscientist-0.0.1b0/EvoScientist/__init__.py +74 -0
- evoscientist-0.0.1b0/EvoScientist/__main__.py +4 -0
- evoscientist-0.0.1b0/EvoScientist/backends.py +405 -0
- evoscientist-0.0.1b0/EvoScientist/channels/__init__.py +44 -0
- evoscientist-0.0.1b0/EvoScientist/channels/base.py +1074 -0
- evoscientist-0.0.1b0/EvoScientist/channels/bus/__init__.py +6 -0
- evoscientist-0.0.1b0/EvoScientist/channels/bus/events.py +52 -0
- evoscientist-0.0.1b0/EvoScientist/channels/bus/message_bus.py +96 -0
- evoscientist-0.0.1b0/EvoScientist/channels/capabilities.py +220 -0
- evoscientist-0.0.1b0/EvoScientist/channels/channel_manager.py +1011 -0
- evoscientist-0.0.1b0/EvoScientist/channels/config.py +126 -0
- evoscientist-0.0.1b0/EvoScientist/channels/consumer.py +414 -0
- evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/__init__.py +29 -0
- evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/channel.py +354 -0
- evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/probe.py +33 -0
- evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/serve.py +92 -0
- evoscientist-0.0.1b0/EvoScientist/channels/discord/__init__.py +19 -0
- evoscientist-0.0.1b0/EvoScientist/channels/discord/channel.py +255 -0
- evoscientist-0.0.1b0/EvoScientist/channels/discord/probe.py +33 -0
- evoscientist-0.0.1b0/EvoScientist/channels/discord/serve.py +93 -0
- evoscientist-0.0.1b0/EvoScientist/channels/email/__init__.py +41 -0
- evoscientist-0.0.1b0/EvoScientist/channels/email/channel.py +380 -0
- evoscientist-0.0.1b0/EvoScientist/channels/email/probe.py +84 -0
- evoscientist-0.0.1b0/EvoScientist/channels/email/serve.py +124 -0
- evoscientist-0.0.1b0/EvoScientist/channels/feishu/__init__.py +22 -0
- evoscientist-0.0.1b0/EvoScientist/channels/feishu/channel.py +825 -0
- evoscientist-0.0.1b0/EvoScientist/channels/feishu/probe.py +39 -0
- evoscientist-0.0.1b0/EvoScientist/channels/feishu/serve.py +113 -0
- evoscientist-0.0.1b0/EvoScientist/channels/formatter.py +287 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/__init__.py +42 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/channel_rpc.py +407 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/probe.py +106 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/rpc_client.py +235 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/serve.py +87 -0
- evoscientist-0.0.1b0/EvoScientist/channels/imessage/targets.py +232 -0
- evoscientist-0.0.1b0/EvoScientist/channels/middleware.py +837 -0
- evoscientist-0.0.1b0/EvoScientist/channels/mixins.py +324 -0
- evoscientist-0.0.1b0/EvoScientist/channels/plugin.py +226 -0
- evoscientist-0.0.1b0/EvoScientist/channels/qq/__init__.py +26 -0
- evoscientist-0.0.1b0/EvoScientist/channels/qq/channel.py +259 -0
- evoscientist-0.0.1b0/EvoScientist/channels/qq/probe.py +37 -0
- evoscientist-0.0.1b0/EvoScientist/channels/qq/serve.py +87 -0
- evoscientist-0.0.1b0/EvoScientist/channels/retry.py +122 -0
- evoscientist-0.0.1b0/EvoScientist/channels/signal/__init__.py +27 -0
- evoscientist-0.0.1b0/EvoScientist/channels/signal/channel.py +462 -0
- evoscientist-0.0.1b0/EvoScientist/channels/signal/probe.py +39 -0
- evoscientist-0.0.1b0/EvoScientist/channels/signal/serve.py +99 -0
- evoscientist-0.0.1b0/EvoScientist/channels/slack/__init__.py +20 -0
- evoscientist-0.0.1b0/EvoScientist/channels/slack/channel.py +291 -0
- evoscientist-0.0.1b0/EvoScientist/channels/slack/probe.py +48 -0
- evoscientist-0.0.1b0/EvoScientist/channels/slack/serve.py +99 -0
- evoscientist-0.0.1b0/EvoScientist/channels/standalone.py +142 -0
- evoscientist-0.0.1b0/EvoScientist/channels/telegram/__init__.py +17 -0
- evoscientist-0.0.1b0/EvoScientist/channels/telegram/channel.py +289 -0
- evoscientist-0.0.1b0/EvoScientist/channels/telegram/probe.py +32 -0
- evoscientist-0.0.1b0/EvoScientist/channels/telegram/serve.py +81 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/__init__.py +69 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/channel.py +865 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/crypto.py +187 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/probe.py +72 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/serve.py +139 -0
- evoscientist-0.0.1b0/EvoScientist/channels/wechat/verify_server.py +175 -0
- evoscientist-0.0.1b0/EvoScientist/cli/__init__.py +37 -0
- evoscientist-0.0.1b0/EvoScientist/cli/_app.py +51 -0
- evoscientist-0.0.1b0/EvoScientist/cli/_constants.py +39 -0
- evoscientist-0.0.1b0/EvoScientist/cli/agent.py +64 -0
- evoscientist-0.0.1b0/EvoScientist/cli/channel.py +482 -0
- evoscientist-0.0.1b0/EvoScientist/cli/clipboard.py +116 -0
- evoscientist-0.0.1b0/EvoScientist/cli/commands.py +595 -0
- evoscientist-0.0.1b0/EvoScientist/cli/interactive.py +771 -0
- evoscientist-0.0.1b0/EvoScientist/cli/mcp_ui.py +282 -0
- evoscientist-0.0.1b0/EvoScientist/cli/skills_cmd.py +86 -0
- evoscientist-0.0.1b0/EvoScientist/cli/tui_backends.py +61 -0
- evoscientist-0.0.1b0/EvoScientist/cli/tui_interactive.py +1826 -0
- evoscientist-0.0.1b0/EvoScientist/cli/tui_runtime.py +99 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/__init__.py +21 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/assistant_message.py +50 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/loading_widget.py +50 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/subagent_widget.py +199 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/system_message.py +20 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/thinking_widget.py +82 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/todo_widget.py +77 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/tool_call_widget.py +215 -0
- evoscientist-0.0.1b0/EvoScientist/cli/widgets/user_message.py +25 -0
- evoscientist-0.0.1b0/EvoScientist/config/__init__.py +38 -0
- evoscientist-0.0.1b0/EvoScientist/config/onboard.py +1926 -0
- evoscientist-0.0.1b0/EvoScientist/config/settings.py +407 -0
- evoscientist-0.0.1b0/EvoScientist/llm/__init__.py +23 -0
- evoscientist-0.0.1b0/EvoScientist/llm/models.py +205 -0
- evoscientist-0.0.1b0/EvoScientist/mcp/__init__.py +32 -0
- evoscientist-0.0.1b0/EvoScientist/mcp/client.py +695 -0
- evoscientist-0.0.1b0/EvoScientist/middleware/__init__.py +21 -0
- evoscientist-0.0.1b0/EvoScientist/middleware/memory.py +770 -0
- evoscientist-0.0.1b0/EvoScientist/middleware/tool_error_handler.py +70 -0
- evoscientist-0.0.1b0/EvoScientist/paths.py +81 -0
- evoscientist-0.0.1b0/EvoScientist/prompts.py +281 -0
- evoscientist-0.0.1b0/EvoScientist/sessions.py +345 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/LICENSE.txt +201 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/SKILL.md +211 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/cli-compatibility.md +23 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/known-issues.md +244 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/mode-discussion.md +93 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/mode-pipeline.md +83 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/reliability.md +245 -0
- evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/task-board.md +63 -0
- evoscientist-0.0.1b0/EvoScientist/skills/find-skills/SKILL.md +76 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/SKILL.md +362 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/references/workflows.md +28 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
- evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
- evoscientist-0.0.1b0/EvoScientist/stream/__init__.py +80 -0
- evoscientist-0.0.1b0/EvoScientist/stream/display.py +837 -0
- evoscientist-0.0.1b0/EvoScientist/stream/emitter.py +94 -0
- evoscientist-0.0.1b0/EvoScientist/stream/events.py +512 -0
- evoscientist-0.0.1b0/EvoScientist/stream/formatter.py +168 -0
- evoscientist-0.0.1b0/EvoScientist/stream/state.py +348 -0
- evoscientist-0.0.1b0/EvoScientist/stream/tracker.py +115 -0
- evoscientist-0.0.1b0/EvoScientist/stream/utils.py +271 -0
- evoscientist-0.0.1b0/EvoScientist/subagent.yaml +147 -0
- evoscientist-0.0.1b0/EvoScientist/tools/__init__.py +16 -0
- evoscientist-0.0.1b0/EvoScientist/tools/search.py +115 -0
- evoscientist-0.0.1b0/EvoScientist/tools/skill_manager.py +120 -0
- evoscientist-0.0.1b0/EvoScientist/tools/skills_manager.py +470 -0
- evoscientist-0.0.1b0/EvoScientist/tools/think.py +32 -0
- evoscientist-0.0.1b0/EvoScientist/utils.py +217 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/PKG-INFO +438 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/SOURCES.txt +171 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/dependency_links.txt +1 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/entry_points.txt +5 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/requires.txt +45 -0
- evoscientist-0.0.1b0/EvoScientist.egg-info/top_level.txt +1 -0
- evoscientist-0.0.1b0/LICENSE +21 -0
- evoscientist-0.0.1b0/PKG-INFO +438 -0
- evoscientist-0.0.1b0/README.md +382 -0
- evoscientist-0.0.1b0/pyproject.toml +91 -0
- evoscientist-0.0.1b0/setup.cfg +4 -0
- evoscientist-0.0.1b0/tests/test_agent_mcp_cache.py +59 -0
- evoscientist-0.0.1b0/tests/test_backends.py +369 -0
- evoscientist-0.0.1b0/tests/test_bus_integration.py +272 -0
- evoscientist-0.0.1b0/tests/test_channel_comprehensive.py +1621 -0
- evoscientist-0.0.1b0/tests/test_cli_channel_bus_mode.py +122 -0
- evoscientist-0.0.1b0/tests/test_cli_run_name.py +38 -0
- evoscientist-0.0.1b0/tests/test_cli_serve.py +143 -0
- evoscientist-0.0.1b0/tests/test_cli_tui_dispatch.py +53 -0
- evoscientist-0.0.1b0/tests/test_config.py +397 -0
- evoscientist-0.0.1b0/tests/test_dingtalk_channel.py +290 -0
- evoscientist-0.0.1b0/tests/test_discord_channel.py +63 -0
- evoscientist-0.0.1b0/tests/test_event_loop.py +200 -0
- evoscientist-0.0.1b0/tests/test_feishu_channel.py +497 -0
- evoscientist-0.0.1b0/tests/test_llm.py +306 -0
- evoscientist-0.0.1b0/tests/test_mcp_client.py +691 -0
- evoscientist-0.0.1b0/tests/test_memory_merge.py +73 -0
- evoscientist-0.0.1b0/tests/test_onboard.py +826 -0
- evoscientist-0.0.1b0/tests/test_paths.py +105 -0
- evoscientist-0.0.1b0/tests/test_prompts.py +35 -0
- evoscientist-0.0.1b0/tests/test_rich_escape.py +18 -0
- evoscientist-0.0.1b0/tests/test_sessions.py +277 -0
- evoscientist-0.0.1b0/tests/test_skills_manager.py +341 -0
- evoscientist-0.0.1b0/tests/test_slack_channel.py +79 -0
- evoscientist-0.0.1b0/tests/test_stream_emitter.py +89 -0
- evoscientist-0.0.1b0/tests/test_stream_events.py +87 -0
- evoscientist-0.0.1b0/tests/test_stream_state.py +558 -0
- evoscientist-0.0.1b0/tests/test_stream_tracker.py +99 -0
- evoscientist-0.0.1b0/tests/test_stream_utils.py +240 -0
- evoscientist-0.0.1b0/tests/test_telegram_channel.py +60 -0
- evoscientist-0.0.1b0/tests/test_tool_error_handler.py +220 -0
- evoscientist-0.0.1b0/tests/test_tools.py +18 -0
- evoscientist-0.0.1b0/tests/test_tui_widgets.py +451 -0
- evoscientist-0.0.1b0/tests/test_ui_runtime.py +58 -0
- evoscientist-0.0.1b0/tests/test_wechat_channel.py +452 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""EvoScientist Agent graph construction.
|
|
2
|
+
|
|
3
|
+
This module creates and exports the compiled agent graph.
|
|
4
|
+
Usage:
|
|
5
|
+
from EvoScientist import agent
|
|
6
|
+
|
|
7
|
+
# Notebook / programmatic usage
|
|
8
|
+
for state in agent.stream(
|
|
9
|
+
{"messages": [HumanMessage(content="your question")]},
|
|
10
|
+
config={"configurable": {"thread_id": "1"}},
|
|
11
|
+
stream_mode="values",
|
|
12
|
+
):
|
|
13
|
+
...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from deepagents import create_deep_agent
|
|
21
|
+
from deepagents.backends import FilesystemBackend, CompositeBackend
|
|
22
|
+
|
|
23
|
+
from .backends import CustomSandboxBackend, MergedReadOnlyBackend
|
|
24
|
+
from .config import get_effective_config, apply_config_to_env
|
|
25
|
+
from .llm import get_chat_model
|
|
26
|
+
from .mcp import load_mcp_tools
|
|
27
|
+
from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware
|
|
28
|
+
from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
|
|
29
|
+
from .utils import load_subagents
|
|
30
|
+
from .tools import tavily_search, think_tool, skill_manager
|
|
31
|
+
from . import paths as _paths_mod
|
|
32
|
+
from .paths import set_active_workspace, set_workspace_root
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Configuration
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
# Load configuration from file/env/defaults
|
|
39
|
+
_config = get_effective_config()
|
|
40
|
+
apply_config_to_env(_config)
|
|
41
|
+
|
|
42
|
+
# NOTE: We intentionally do NOT call set_workspace_root() at module level.
|
|
43
|
+
# The CLI (commands.py) calls set_workspace_root() *before* importing this
|
|
44
|
+
# module. A module-level call here would overwrite the CLI's --workdir
|
|
45
|
+
# value with config.default_workdir, violating the priority chain
|
|
46
|
+
# (CLI args > config file). Instead, config.default_workdir is applied
|
|
47
|
+
# as a fallback inside create_cli_agent() when no explicit workspace_dir
|
|
48
|
+
# is provided.
|
|
49
|
+
|
|
50
|
+
# Research limits (from config)
|
|
51
|
+
MAX_CONCURRENT = _config.max_concurrent
|
|
52
|
+
MAX_ITERATIONS = _config.max_iterations
|
|
53
|
+
|
|
54
|
+
# Workspace settings (defer dir creation to CLI; here we just resolve paths)
|
|
55
|
+
# Read from the paths module so values reflect any earlier set_workspace_root().
|
|
56
|
+
WORKSPACE_DIR = str(_paths_mod.WORKSPACE_ROOT)
|
|
57
|
+
set_active_workspace(WORKSPACE_DIR)
|
|
58
|
+
MEMORY_DIR = str(_paths_mod.MEMORY_DIR) # Shared across sessions (not per-session)
|
|
59
|
+
SKILLS_DIR = str(Path(__file__).parent / "skills")
|
|
60
|
+
USER_SKILLS_DIR = str(_paths_mod.USER_SKILLS_DIR)
|
|
61
|
+
SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Initialization
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Generate system prompt with limits
|
|
69
|
+
SYSTEM_PROMPT = get_system_prompt(
|
|
70
|
+
max_concurrent=MAX_CONCURRENT,
|
|
71
|
+
max_iterations=MAX_ITERATIONS,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Initialize chat model using the LLM module (respects config settings)
|
|
75
|
+
chat_model = get_chat_model(
|
|
76
|
+
model=_config.model,
|
|
77
|
+
provider=_config.provider,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Initialize workspace backend
|
|
81
|
+
_workspace_backend = CustomSandboxBackend(
|
|
82
|
+
root_dir=WORKSPACE_DIR,
|
|
83
|
+
virtual_mode=True,
|
|
84
|
+
timeout=300,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Skills backend: merge user-installed (./skills/) and system (package) skills
|
|
88
|
+
_skills_backend = MergedReadOnlyBackend(
|
|
89
|
+
primary_dir=USER_SKILLS_DIR, # user-installed, takes priority
|
|
90
|
+
secondary_dir=SKILLS_DIR, # package built-in, fallback
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Memory backend: persistent filesystem for long-term memory (shared across sessions)
|
|
94
|
+
_memory_backend = FilesystemBackend(
|
|
95
|
+
root_dir=MEMORY_DIR,
|
|
96
|
+
virtual_mode=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Composite backend: workspace as default, skills and memory mounted
|
|
100
|
+
backend = CompositeBackend(
|
|
101
|
+
default=_workspace_backend,
|
|
102
|
+
routes={
|
|
103
|
+
"/skills/": _skills_backend,
|
|
104
|
+
"/memory/": _memory_backend,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
tool_registry = {
|
|
109
|
+
"think_tool": think_tool,
|
|
110
|
+
"tavily_search": tavily_search,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Base tools that every agent variant gets (before MCP)
|
|
114
|
+
BASE_TOOLS = [think_tool, skill_manager]
|
|
115
|
+
|
|
116
|
+
# Cache MCP tools by the effective config signature to avoid reconnecting
|
|
117
|
+
# to MCP servers on every `/new` when config is unchanged.
|
|
118
|
+
_MCP_TOOLS_CACHE_KEY: str | None = None
|
|
119
|
+
_MCP_TOOLS_CACHE_VALUE: dict[str, list] | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _mcp_config_signature() -> str:
|
|
123
|
+
"""Return a stable signature for the effective MCP config."""
|
|
124
|
+
from .mcp.client import load_mcp_config
|
|
125
|
+
|
|
126
|
+
cfg = load_mcp_config()
|
|
127
|
+
if not cfg:
|
|
128
|
+
return ""
|
|
129
|
+
try:
|
|
130
|
+
return json.dumps(cfg, sort_keys=True, ensure_ascii=True)
|
|
131
|
+
except TypeError:
|
|
132
|
+
# Fallback for non-JSON-serializable values (should be rare)
|
|
133
|
+
return repr(cfg)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_mcp_tools_cached() -> dict[str, list]:
|
|
137
|
+
"""Load MCP tools with config-aware caching."""
|
|
138
|
+
global _MCP_TOOLS_CACHE_KEY, _MCP_TOOLS_CACHE_VALUE
|
|
139
|
+
|
|
140
|
+
cfg_key = _mcp_config_signature()
|
|
141
|
+
if not cfg_key:
|
|
142
|
+
_MCP_TOOLS_CACHE_KEY = ""
|
|
143
|
+
_MCP_TOOLS_CACHE_VALUE = {}
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
if _MCP_TOOLS_CACHE_KEY == cfg_key and _MCP_TOOLS_CACHE_VALUE is not None:
|
|
147
|
+
return {k: list(v) for k, v in _MCP_TOOLS_CACHE_VALUE.items()}
|
|
148
|
+
|
|
149
|
+
loaded = load_mcp_tools()
|
|
150
|
+
_MCP_TOOLS_CACHE_KEY = cfg_key
|
|
151
|
+
_MCP_TOOLS_CACHE_VALUE = {k: list(v) for k, v in loaded.items()}
|
|
152
|
+
return {k: list(v) for k, v in loaded.items()}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _inject_subagent_middleware(subs: list[dict]) -> None:
|
|
156
|
+
"""Ensure every subagent gets ToolErrorHandlerMiddleware.
|
|
157
|
+
|
|
158
|
+
Without this, subagent tool errors are caught by LangGraph's default
|
|
159
|
+
ToolNode handler which produces terse messages without tracebacks or
|
|
160
|
+
retry guidance — reducing the subagent's ability to self-recover.
|
|
161
|
+
"""
|
|
162
|
+
for sa in subs:
|
|
163
|
+
sa.setdefault("middleware", []).append(ToolErrorHandlerMiddleware())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_base_kwargs(base_backend, base_middleware):
|
|
167
|
+
"""Build agent kwargs *without* MCP (fast, no subprocess spawning)."""
|
|
168
|
+
subs = load_subagents(
|
|
169
|
+
SUBAGENTS_CONFIG,
|
|
170
|
+
tool_registry=tool_registry,
|
|
171
|
+
prompt_refs=_build_prompt_refs(),
|
|
172
|
+
)
|
|
173
|
+
_inject_subagent_middleware(subs)
|
|
174
|
+
return dict(
|
|
175
|
+
name="EvoScientist",
|
|
176
|
+
model=chat_model,
|
|
177
|
+
tools=list(BASE_TOOLS),
|
|
178
|
+
backend=base_backend,
|
|
179
|
+
subagents=subs,
|
|
180
|
+
middleware=base_middleware,
|
|
181
|
+
system_prompt=SYSTEM_PROMPT,
|
|
182
|
+
skills=["/skills/"],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_mcp_and_build_kwargs(base_backend, base_middleware):
|
|
187
|
+
"""Load MCP tools (cached by config) and build agent kwargs.
|
|
188
|
+
|
|
189
|
+
Re-connects to MCP servers only when the effective MCP config changes.
|
|
190
|
+
Falls back to base kwargs if no MCP configured.
|
|
191
|
+
"""
|
|
192
|
+
mcp_by_agent = _load_mcp_tools_cached()
|
|
193
|
+
if not mcp_by_agent:
|
|
194
|
+
return _build_base_kwargs(base_backend, base_middleware)
|
|
195
|
+
|
|
196
|
+
# Fresh tool registry — start from base tools + MCP tools
|
|
197
|
+
registry = dict(tool_registry)
|
|
198
|
+
for tools in mcp_by_agent.values():
|
|
199
|
+
for t in tools:
|
|
200
|
+
registry[t.name] = t
|
|
201
|
+
|
|
202
|
+
mcp_main = mcp_by_agent.pop("main", [])
|
|
203
|
+
|
|
204
|
+
subs = load_subagents(
|
|
205
|
+
SUBAGENTS_CONFIG,
|
|
206
|
+
tool_registry=registry,
|
|
207
|
+
prompt_refs=_build_prompt_refs(),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
_inject_subagent_middleware(subs)
|
|
211
|
+
|
|
212
|
+
# Inject MCP tools into subagents by name
|
|
213
|
+
for sa in subs:
|
|
214
|
+
if sa_tools := mcp_by_agent.get(sa["name"], []):
|
|
215
|
+
sa.setdefault("tools", []).extend(sa_tools)
|
|
216
|
+
|
|
217
|
+
return dict(
|
|
218
|
+
name="EvoScientist",
|
|
219
|
+
model=chat_model,
|
|
220
|
+
tools=BASE_TOOLS + mcp_main,
|
|
221
|
+
backend=base_backend,
|
|
222
|
+
subagents=subs,
|
|
223
|
+
middleware=base_middleware,
|
|
224
|
+
system_prompt=SYSTEM_PROMPT,
|
|
225
|
+
skills=["/skills/"],
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _build_prompt_refs() -> dict:
|
|
230
|
+
"""Build prompt references with the current date (not frozen at import)."""
|
|
231
|
+
return {
|
|
232
|
+
"RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(
|
|
233
|
+
date=datetime.now().strftime("%Y-%m-%d"),
|
|
234
|
+
),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
base_middleware = [
|
|
238
|
+
ToolErrorHandlerMiddleware(),
|
|
239
|
+
create_memory_middleware(MEMORY_DIR, extraction_model=chat_model),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
|
|
243
|
+
# Lazily constructed on first access so MCP tools are included without
|
|
244
|
+
# spawning subprocesses at import time.
|
|
245
|
+
_EvoScientist_agent = None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _get_default_agent():
|
|
249
|
+
"""Build the default agent (with MCP, no checkpointer) on first access."""
|
|
250
|
+
global _EvoScientist_agent
|
|
251
|
+
if _EvoScientist_agent is None:
|
|
252
|
+
kwargs = load_mcp_and_build_kwargs(backend, base_middleware)
|
|
253
|
+
_EvoScientist_agent = create_deep_agent(**kwargs).with_config(
|
|
254
|
+
{"recursion_limit": 500}
|
|
255
|
+
)
|
|
256
|
+
return _EvoScientist_agent
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def __getattr__(name: str):
|
|
260
|
+
if name == "EvoScientist_agent":
|
|
261
|
+
return _get_default_agent()
|
|
262
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def create_cli_agent(workspace_dir: str | None = None, checkpointer=None):
|
|
266
|
+
"""Create agent with checkpointer for CLI multi-turn support.
|
|
267
|
+
|
|
268
|
+
A fresh backend is constructed on every call using the current
|
|
269
|
+
``paths.WORKSPACE_ROOT`` (or the explicit *workspace_dir*), so
|
|
270
|
+
runtime ``set_workspace_root()`` changes are always respected.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
workspace_dir: Per-session workspace directory. If ``None``,
|
|
274
|
+
defaults to the current ``paths.WORKSPACE_ROOT``.
|
|
275
|
+
checkpointer: Optional LangGraph checkpointer. If ``None``,
|
|
276
|
+
falls back to ``InMemorySaver`` (non-persistent).
|
|
277
|
+
"""
|
|
278
|
+
import os as _os
|
|
279
|
+
from . import paths as _paths
|
|
280
|
+
|
|
281
|
+
if checkpointer is None:
|
|
282
|
+
from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
|
|
283
|
+
checkpointer = InMemorySaver()
|
|
284
|
+
|
|
285
|
+
# When no explicit workspace_dir is provided, apply config.default_workdir
|
|
286
|
+
# as a fallback. This covers direct callers (notebooks, iMessage server)
|
|
287
|
+
# that never call set_workspace_root() themselves. CLI callers always
|
|
288
|
+
# pass workspace_dir explicitly, so their --workdir is never overwritten.
|
|
289
|
+
if workspace_dir is None:
|
|
290
|
+
if _config.default_workdir:
|
|
291
|
+
set_workspace_root(
|
|
292
|
+
_os.path.abspath(_os.path.expanduser(_config.default_workdir))
|
|
293
|
+
)
|
|
294
|
+
workspace_dir = str(_paths.WORKSPACE_ROOT)
|
|
295
|
+
|
|
296
|
+
# Read paths dynamically so runtime set_workspace_root() changes are picked up
|
|
297
|
+
_mem_dir = str(_paths.MEMORY_DIR)
|
|
298
|
+
_usr_skills_dir = str(_paths.USER_SKILLS_DIR)
|
|
299
|
+
|
|
300
|
+
# Always construct fresh backends from current paths (avoids stale
|
|
301
|
+
# module-level backend when workspace root changed at runtime).
|
|
302
|
+
set_active_workspace(workspace_dir)
|
|
303
|
+
ws_backend = CustomSandboxBackend(
|
|
304
|
+
root_dir=workspace_dir,
|
|
305
|
+
virtual_mode=True,
|
|
306
|
+
timeout=300,
|
|
307
|
+
)
|
|
308
|
+
sk_backend = MergedReadOnlyBackend(
|
|
309
|
+
primary_dir=_usr_skills_dir,
|
|
310
|
+
secondary_dir=SKILLS_DIR,
|
|
311
|
+
)
|
|
312
|
+
# Memory always uses SHARED directory (not per-session) for cross-session persistence
|
|
313
|
+
mem_backend = FilesystemBackend(
|
|
314
|
+
root_dir=_mem_dir,
|
|
315
|
+
virtual_mode=True,
|
|
316
|
+
)
|
|
317
|
+
be = CompositeBackend(
|
|
318
|
+
default=ws_backend,
|
|
319
|
+
routes={
|
|
320
|
+
"/skills/": sk_backend,
|
|
321
|
+
"/memory/": mem_backend,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
mw = [
|
|
326
|
+
ToolErrorHandlerMiddleware(),
|
|
327
|
+
create_memory_middleware(_mem_dir, extraction_model=chat_model),
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
# Re-load MCP tools from current config (picks up /mcp add changes)
|
|
331
|
+
kwargs = load_mcp_and_build_kwargs(be, mw)
|
|
332
|
+
|
|
333
|
+
return create_deep_agent(
|
|
334
|
+
**kwargs,
|
|
335
|
+
checkpointer=checkpointer,
|
|
336
|
+
).with_config({"recursion_limit": 500})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""EvoScientist Agent - AI-powered research and code execution.
|
|
2
|
+
|
|
3
|
+
This package exposes a convenience API at the package root while keeping
|
|
4
|
+
imports lazy, so lightweight modules (for example config helpers) can be used
|
|
5
|
+
without importing heavy runtime dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib import import_module
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_EXPORTS: dict[str, tuple[str, str]] = {
|
|
14
|
+
# Agent graph (lazy to avoid expensive initialization at import time)
|
|
15
|
+
"EvoScientist_agent": (".EvoScientist", "EvoScientist_agent"),
|
|
16
|
+
"create_cli_agent": (".EvoScientist", "create_cli_agent"),
|
|
17
|
+
# Backends
|
|
18
|
+
"CustomSandboxBackend": (".backends", "CustomSandboxBackend"),
|
|
19
|
+
"ReadOnlyFilesystemBackend": (".backends", "ReadOnlyFilesystemBackend"),
|
|
20
|
+
# Configuration
|
|
21
|
+
"EvoScientistConfig": (".config", "EvoScientistConfig"),
|
|
22
|
+
"load_config": (".config", "load_config"),
|
|
23
|
+
"save_config": (".config", "save_config"),
|
|
24
|
+
"get_effective_config": (".config", "get_effective_config"),
|
|
25
|
+
"get_config_path": (".config", "get_config_path"),
|
|
26
|
+
# LLM
|
|
27
|
+
"get_chat_model": (".llm", "get_chat_model"),
|
|
28
|
+
"MODELS": (".llm", "MODELS"),
|
|
29
|
+
"list_models": (".llm", "list_models"),
|
|
30
|
+
"DEFAULT_MODEL": (".llm", "DEFAULT_MODEL"),
|
|
31
|
+
# Prompts
|
|
32
|
+
"get_system_prompt": (".prompts", "get_system_prompt"),
|
|
33
|
+
"RESEARCHER_INSTRUCTIONS": (".prompts", "RESEARCHER_INSTRUCTIONS"),
|
|
34
|
+
# Tools
|
|
35
|
+
"tavily_search": (".tools", "tavily_search"),
|
|
36
|
+
"think_tool": (".tools", "think_tool"),
|
|
37
|
+
# Sessions
|
|
38
|
+
"get_checkpointer": (".sessions", "get_checkpointer"),
|
|
39
|
+
"generate_thread_id": (".sessions", "generate_thread_id"),
|
|
40
|
+
"list_threads": (".sessions", "list_threads"),
|
|
41
|
+
"delete_thread": (".sessions", "delete_thread"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str):
|
|
46
|
+
"""Lazily import and cache package-level attributes.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
name: The attribute name to look up.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The resolved attribute value.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
AttributeError: If the name is not in _EXPORTS.
|
|
56
|
+
"""
|
|
57
|
+
target = _EXPORTS.get(name)
|
|
58
|
+
if target is None:
|
|
59
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
60
|
+
|
|
61
|
+
module_name, attr_name = target
|
|
62
|
+
module = import_module(module_name, package=__name__)
|
|
63
|
+
value = getattr(module, attr_name)
|
|
64
|
+
# Cache after first load to avoid repeated import lookups.
|
|
65
|
+
globals()[name] = value
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def __dir__() -> list[str]:
|
|
70
|
+
"""List available public attributes including lazy exports."""
|
|
71
|
+
return sorted(set(globals()) | set(_EXPORTS))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = list(_EXPORTS)
|