EvoScientist 0.0.1__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.1/EvoScientist/EvoScientist.py +413 -0
- evoscientist-0.0.1/EvoScientist/__init__.py +74 -0
- evoscientist-0.0.1/EvoScientist/__main__.py +4 -0
- evoscientist-0.0.1/EvoScientist/backends.py +425 -0
- evoscientist-0.0.1/EvoScientist/channels/__init__.py +44 -0
- evoscientist-0.0.1/EvoScientist/channels/base.py +1074 -0
- evoscientist-0.0.1/EvoScientist/channels/bus/__init__.py +6 -0
- evoscientist-0.0.1/EvoScientist/channels/bus/events.py +52 -0
- evoscientist-0.0.1/EvoScientist/channels/bus/message_bus.py +96 -0
- evoscientist-0.0.1/EvoScientist/channels/capabilities.py +220 -0
- evoscientist-0.0.1/EvoScientist/channels/channel_manager.py +1011 -0
- evoscientist-0.0.1/EvoScientist/channels/config.py +126 -0
- evoscientist-0.0.1/EvoScientist/channels/consumer.py +769 -0
- evoscientist-0.0.1/EvoScientist/channels/dingtalk/__init__.py +29 -0
- evoscientist-0.0.1/EvoScientist/channels/dingtalk/channel.py +354 -0
- evoscientist-0.0.1/EvoScientist/channels/dingtalk/probe.py +33 -0
- evoscientist-0.0.1/EvoScientist/channels/dingtalk/serve.py +92 -0
- evoscientist-0.0.1/EvoScientist/channels/discord/__init__.py +19 -0
- evoscientist-0.0.1/EvoScientist/channels/discord/channel.py +255 -0
- evoscientist-0.0.1/EvoScientist/channels/discord/probe.py +33 -0
- evoscientist-0.0.1/EvoScientist/channels/discord/serve.py +93 -0
- evoscientist-0.0.1/EvoScientist/channels/email/__init__.py +41 -0
- evoscientist-0.0.1/EvoScientist/channels/email/channel.py +380 -0
- evoscientist-0.0.1/EvoScientist/channels/email/probe.py +84 -0
- evoscientist-0.0.1/EvoScientist/channels/email/serve.py +124 -0
- evoscientist-0.0.1/EvoScientist/channels/feishu/__init__.py +22 -0
- evoscientist-0.0.1/EvoScientist/channels/feishu/channel.py +825 -0
- evoscientist-0.0.1/EvoScientist/channels/feishu/probe.py +39 -0
- evoscientist-0.0.1/EvoScientist/channels/feishu/serve.py +113 -0
- evoscientist-0.0.1/EvoScientist/channels/formatter.py +287 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/__init__.py +42 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/channel_rpc.py +407 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/probe.py +106 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/rpc_client.py +235 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/serve.py +87 -0
- evoscientist-0.0.1/EvoScientist/channels/imessage/targets.py +232 -0
- evoscientist-0.0.1/EvoScientist/channels/middleware.py +837 -0
- evoscientist-0.0.1/EvoScientist/channels/mixins.py +324 -0
- evoscientist-0.0.1/EvoScientist/channels/plugin.py +226 -0
- evoscientist-0.0.1/EvoScientist/channels/qq/__init__.py +26 -0
- evoscientist-0.0.1/EvoScientist/channels/qq/channel.py +259 -0
- evoscientist-0.0.1/EvoScientist/channels/qq/probe.py +37 -0
- evoscientist-0.0.1/EvoScientist/channels/qq/serve.py +87 -0
- evoscientist-0.0.1/EvoScientist/channels/retry.py +122 -0
- evoscientist-0.0.1/EvoScientist/channels/signal/__init__.py +27 -0
- evoscientist-0.0.1/EvoScientist/channels/signal/channel.py +462 -0
- evoscientist-0.0.1/EvoScientist/channels/signal/probe.py +39 -0
- evoscientist-0.0.1/EvoScientist/channels/signal/serve.py +99 -0
- evoscientist-0.0.1/EvoScientist/channels/slack/__init__.py +20 -0
- evoscientist-0.0.1/EvoScientist/channels/slack/channel.py +291 -0
- evoscientist-0.0.1/EvoScientist/channels/slack/probe.py +48 -0
- evoscientist-0.0.1/EvoScientist/channels/slack/serve.py +99 -0
- evoscientist-0.0.1/EvoScientist/channels/standalone.py +142 -0
- evoscientist-0.0.1/EvoScientist/channels/telegram/__init__.py +17 -0
- evoscientist-0.0.1/EvoScientist/channels/telegram/channel.py +289 -0
- evoscientist-0.0.1/EvoScientist/channels/telegram/probe.py +32 -0
- evoscientist-0.0.1/EvoScientist/channels/telegram/serve.py +81 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/__init__.py +69 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/channel.py +865 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/crypto.py +187 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/probe.py +72 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/serve.py +139 -0
- evoscientist-0.0.1/EvoScientist/channels/wechat/verify_server.py +175 -0
- evoscientist-0.0.1/EvoScientist/cli/__init__.py +37 -0
- evoscientist-0.0.1/EvoScientist/cli/_app.py +51 -0
- evoscientist-0.0.1/EvoScientist/cli/_constants.py +41 -0
- evoscientist-0.0.1/EvoScientist/cli/agent.py +66 -0
- evoscientist-0.0.1/EvoScientist/cli/channel.py +733 -0
- evoscientist-0.0.1/EvoScientist/cli/clipboard.py +116 -0
- evoscientist-0.0.1/EvoScientist/cli/commands.py +738 -0
- evoscientist-0.0.1/EvoScientist/cli/history_suggester.py +85 -0
- evoscientist-0.0.1/EvoScientist/cli/interactive.py +795 -0
- evoscientist-0.0.1/EvoScientist/cli/mcp_ui.py +282 -0
- evoscientist-0.0.1/EvoScientist/cli/skills_cmd.py +98 -0
- evoscientist-0.0.1/EvoScientist/cli/tui_backends.py +67 -0
- evoscientist-0.0.1/EvoScientist/cli/tui_interactive.py +2255 -0
- evoscientist-0.0.1/EvoScientist/cli/tui_runtime.py +107 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/__init__.py +31 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/approval_widget.py +211 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/ask_user_widget.py +375 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/assistant_message.py +59 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/loading_widget.py +50 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/subagent_widget.py +293 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/summarization_widget.py +92 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/system_message.py +20 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/thinking_widget.py +117 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/thread_selector.py +197 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/todo_widget.py +77 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/tool_call_widget.py +268 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/usage_widget.py +29 -0
- evoscientist-0.0.1/EvoScientist/cli/widgets/user_message.py +25 -0
- evoscientist-0.0.1/EvoScientist/config/__init__.py +47 -0
- evoscientist-0.0.1/EvoScientist/config/onboard.py +2157 -0
- evoscientist-0.0.1/EvoScientist/config/settings.py +420 -0
- evoscientist-0.0.1/EvoScientist/llm/__init__.py +23 -0
- evoscientist-0.0.1/EvoScientist/llm/models.py +251 -0
- evoscientist-0.0.1/EvoScientist/mcp/__init__.py +32 -0
- evoscientist-0.0.1/EvoScientist/mcp/client.py +695 -0
- evoscientist-0.0.1/EvoScientist/middleware/__init__.py +33 -0
- evoscientist-0.0.1/EvoScientist/middleware/ask_user.py +416 -0
- evoscientist-0.0.1/EvoScientist/middleware/memory.py +786 -0
- evoscientist-0.0.1/EvoScientist/middleware/tool_error_handler.py +80 -0
- evoscientist-0.0.1/EvoScientist/paths.py +81 -0
- evoscientist-0.0.1/EvoScientist/prompts.py +339 -0
- evoscientist-0.0.1/EvoScientist/sessions.py +345 -0
- evoscientist-0.0.1/EvoScientist/skills/find-skills/SKILL.md +76 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/LICENSE.txt +205 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/SKILL.md +437 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/analyzer.md +274 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/comparator.md +202 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/grader.md +223 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/assets/eval_review.html +147 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/eval-viewer/viewer.html +1335 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/schemas.md +430 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/workflows.md +28 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__init__.py +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/generate_report.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/improve_description.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/init_skill.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/package_skill.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/run_eval.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/run_loop.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/utils.cpython-311.pyc +0 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/aggregate_benchmark.py +416 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/generate_report.py +320 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/improve_description.py +285 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/package_skill.py +139 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/quick_validate.py +119 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/run_eval.py +241 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/run_loop.py +335 -0
- evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/utils.py +47 -0
- evoscientist-0.0.1/EvoScientist/stream/__init__.py +84 -0
- evoscientist-0.0.1/EvoScientist/stream/diff_format.py +207 -0
- evoscientist-0.0.1/EvoScientist/stream/display.py +1271 -0
- evoscientist-0.0.1/EvoScientist/stream/emitter.py +132 -0
- evoscientist-0.0.1/EvoScientist/stream/events.py +592 -0
- evoscientist-0.0.1/EvoScientist/stream/formatter.py +168 -0
- evoscientist-0.0.1/EvoScientist/stream/state.py +384 -0
- evoscientist-0.0.1/EvoScientist/stream/tracker.py +115 -0
- evoscientist-0.0.1/EvoScientist/stream/utils.py +266 -0
- evoscientist-0.0.1/EvoScientist/subagent.yaml +160 -0
- evoscientist-0.0.1/EvoScientist/tools/__init__.py +16 -0
- evoscientist-0.0.1/EvoScientist/tools/search.py +115 -0
- evoscientist-0.0.1/EvoScientist/tools/skill_manager.py +120 -0
- evoscientist-0.0.1/EvoScientist/tools/skills_manager.py +504 -0
- evoscientist-0.0.1/EvoScientist/tools/think.py +58 -0
- evoscientist-0.0.1/EvoScientist/utils.py +217 -0
- evoscientist-0.0.1/EvoScientist.egg-info/PKG-INFO +565 -0
- evoscientist-0.0.1/EvoScientist.egg-info/SOURCES.txt +202 -0
- evoscientist-0.0.1/EvoScientist.egg-info/dependency_links.txt +1 -0
- evoscientist-0.0.1/EvoScientist.egg-info/entry_points.txt +5 -0
- evoscientist-0.0.1/EvoScientist.egg-info/requires.txt +45 -0
- evoscientist-0.0.1/EvoScientist.egg-info/top_level.txt +1 -0
- evoscientist-0.0.1/LICENSE +201 -0
- evoscientist-0.0.1/PKG-INFO +565 -0
- evoscientist-0.0.1/README.md +509 -0
- evoscientist-0.0.1/pyproject.toml +91 -0
- evoscientist-0.0.1/setup.cfg +4 -0
- evoscientist-0.0.1/tests/test_additional_channel_smoke.py +96 -0
- evoscientist-0.0.1/tests/test_agent_mcp_cache.py +59 -0
- evoscientist-0.0.1/tests/test_ask_user.py +514 -0
- evoscientist-0.0.1/tests/test_backends.py +397 -0
- evoscientist-0.0.1/tests/test_bus_integration.py +272 -0
- evoscientist-0.0.1/tests/test_channel_comprehensive.py +1621 -0
- evoscientist-0.0.1/tests/test_cli_channel_bus_mode.py +122 -0
- evoscientist-0.0.1/tests/test_cli_run_name.py +38 -0
- evoscientist-0.0.1/tests/test_cli_serve.py +144 -0
- evoscientist-0.0.1/tests/test_cli_tui_dispatch.py +53 -0
- evoscientist-0.0.1/tests/test_config.py +383 -0
- evoscientist-0.0.1/tests/test_diff_format.py +246 -0
- evoscientist-0.0.1/tests/test_dingtalk_channel.py +290 -0
- evoscientist-0.0.1/tests/test_discord_channel.py +63 -0
- evoscientist-0.0.1/tests/test_event_loop.py +200 -0
- evoscientist-0.0.1/tests/test_feishu_channel.py +497 -0
- evoscientist-0.0.1/tests/test_hitl.py +532 -0
- evoscientist-0.0.1/tests/test_llm.py +464 -0
- evoscientist-0.0.1/tests/test_mcp_client.py +691 -0
- evoscientist-0.0.1/tests/test_memory_merge.py +73 -0
- evoscientist-0.0.1/tests/test_onboard.py +818 -0
- evoscientist-0.0.1/tests/test_paths.py +105 -0
- evoscientist-0.0.1/tests/test_prompts.py +37 -0
- evoscientist-0.0.1/tests/test_rich_escape.py +18 -0
- evoscientist-0.0.1/tests/test_sessions.py +301 -0
- evoscientist-0.0.1/tests/test_skills_manager.py +415 -0
- evoscientist-0.0.1/tests/test_slack_channel.py +79 -0
- evoscientist-0.0.1/tests/test_stream_emitter.py +105 -0
- evoscientist-0.0.1/tests/test_stream_events.py +215 -0
- evoscientist-0.0.1/tests/test_stream_state.py +595 -0
- evoscientist-0.0.1/tests/test_stream_tracker.py +99 -0
- evoscientist-0.0.1/tests/test_stream_utils.py +232 -0
- evoscientist-0.0.1/tests/test_summarization.py +169 -0
- evoscientist-0.0.1/tests/test_telegram_channel.py +60 -0
- evoscientist-0.0.1/tests/test_thread_selector.py +204 -0
- evoscientist-0.0.1/tests/test_tool_error_handler.py +220 -0
- evoscientist-0.0.1/tests/test_tools.py +22 -0
- evoscientist-0.0.1/tests/test_tui_widgets.py +451 -0
- evoscientist-0.0.1/tests/test_ui_runtime.py +65 -0
- evoscientist-0.0.1/tests/test_wechat_channel.py +452 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""EvoScientist Agent graph construction.
|
|
2
|
+
|
|
3
|
+
This module defines the agent graph and its factory functions. All heavy
|
|
4
|
+
initialization (deepagents, backends, LLM, middleware) is deferred to first
|
|
5
|
+
use so that importing this module is fast and non-agent CLI commands
|
|
6
|
+
(``EvoSci config list``, ``EvoSci onboard``) never pay the cost.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from EvoScientist import EvoScientist_agent
|
|
10
|
+
|
|
11
|
+
# Notebook / programmatic usage
|
|
12
|
+
for state in EvoScientist_agent.stream(
|
|
13
|
+
{"messages": [HumanMessage(content="your question")]},
|
|
14
|
+
config={"configurable": {"thread_id": "1"}},
|
|
15
|
+
stream_mode="values",
|
|
16
|
+
):
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from .config import get_effective_config, apply_config_to_env
|
|
26
|
+
from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
|
|
27
|
+
from . import paths as _paths_mod
|
|
28
|
+
from .paths import set_active_workspace, set_workspace_root
|
|
29
|
+
|
|
30
|
+
# Suppress noisy warnings from deepagents skill loader (non-string frontmatter fields, etc.)
|
|
31
|
+
logging.getLogger("deepagents.middleware.skills").setLevel(logging.ERROR)
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Constants
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
|
|
38
|
+
SKILLS_DIR = str(Path(__file__).parent / "skills")
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Lazy state — initialized on first use, not at import time
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
_config = None
|
|
45
|
+
_chat_model = None
|
|
46
|
+
_system_prompt = None
|
|
47
|
+
|
|
48
|
+
# Cache MCP tools by the effective config signature to avoid reconnecting
|
|
49
|
+
# to MCP servers on every `/new` when config is unchanged.
|
|
50
|
+
_MCP_TOOLS_CACHE_KEY: str | None = None
|
|
51
|
+
_MCP_TOOLS_CACHE_VALUE: dict[str, list] | None = None
|
|
52
|
+
|
|
53
|
+
# Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
|
|
54
|
+
# Lazily constructed on first access so MCP tools are included without
|
|
55
|
+
# spawning subprocesses at import time.
|
|
56
|
+
_EvoScientist_agent = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Lazy initialization helpers
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _ensure_config(config=None):
|
|
65
|
+
"""Return cached config. If *config* is passed, cache and use it."""
|
|
66
|
+
global _config
|
|
67
|
+
if config is not None:
|
|
68
|
+
_config = config
|
|
69
|
+
apply_config_to_env(_config)
|
|
70
|
+
if _config is None:
|
|
71
|
+
_config = get_effective_config()
|
|
72
|
+
apply_config_to_env(_config)
|
|
73
|
+
return _config
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_chat_model():
|
|
77
|
+
"""Return cached chat model, creating it on first call."""
|
|
78
|
+
global _chat_model
|
|
79
|
+
if _chat_model is None:
|
|
80
|
+
from .llm import get_chat_model
|
|
81
|
+
|
|
82
|
+
cfg = _ensure_config()
|
|
83
|
+
_chat_model = get_chat_model(model=cfg.model, provider=cfg.provider)
|
|
84
|
+
return _chat_model
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ensure_system_prompt():
|
|
88
|
+
"""Return cached system prompt, creating it on first call."""
|
|
89
|
+
global _system_prompt
|
|
90
|
+
if _system_prompt is None:
|
|
91
|
+
_system_prompt = get_system_prompt()
|
|
92
|
+
return _system_prompt
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# MCP caching
|
|
97
|
+
# =============================================================================
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _mcp_config_signature() -> str:
|
|
101
|
+
"""Return a stable signature for the effective MCP config."""
|
|
102
|
+
from .mcp.client import load_mcp_config
|
|
103
|
+
|
|
104
|
+
cfg = load_mcp_config()
|
|
105
|
+
if not cfg:
|
|
106
|
+
return ""
|
|
107
|
+
try:
|
|
108
|
+
return json.dumps(cfg, sort_keys=True, ensure_ascii=True)
|
|
109
|
+
except TypeError:
|
|
110
|
+
# Fallback for non-JSON-serializable values (should be rare)
|
|
111
|
+
return repr(cfg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _load_mcp_tools_cached() -> dict[str, list]:
|
|
115
|
+
"""Load MCP tools with config-aware caching."""
|
|
116
|
+
global _MCP_TOOLS_CACHE_KEY, _MCP_TOOLS_CACHE_VALUE
|
|
117
|
+
|
|
118
|
+
from .mcp import load_mcp_tools
|
|
119
|
+
|
|
120
|
+
cfg_key = _mcp_config_signature()
|
|
121
|
+
if not cfg_key:
|
|
122
|
+
_MCP_TOOLS_CACHE_KEY = ""
|
|
123
|
+
_MCP_TOOLS_CACHE_VALUE = {}
|
|
124
|
+
return {}
|
|
125
|
+
|
|
126
|
+
if _MCP_TOOLS_CACHE_KEY == cfg_key and _MCP_TOOLS_CACHE_VALUE is not None:
|
|
127
|
+
return {k: list(v) for k, v in _MCP_TOOLS_CACHE_VALUE.items()}
|
|
128
|
+
|
|
129
|
+
loaded = load_mcp_tools()
|
|
130
|
+
_MCP_TOOLS_CACHE_KEY = cfg_key
|
|
131
|
+
_MCP_TOOLS_CACHE_VALUE = {k: list(v) for k, v in loaded.items()}
|
|
132
|
+
return {k: list(v) for k, v in loaded.items()}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Agent construction helpers
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _inject_subagent_middleware(subs: list[dict]) -> None:
|
|
141
|
+
"""Ensure every subagent gets ToolErrorHandlerMiddleware.
|
|
142
|
+
|
|
143
|
+
Without this, subagent tool errors are caught by LangGraph's default
|
|
144
|
+
ToolNode handler which produces terse messages without tracebacks or
|
|
145
|
+
retry guidance — reducing the subagent's ability to self-recover.
|
|
146
|
+
"""
|
|
147
|
+
from .middleware import ToolErrorHandlerMiddleware
|
|
148
|
+
|
|
149
|
+
for sa in subs:
|
|
150
|
+
sa.setdefault("middleware", []).append(ToolErrorHandlerMiddleware())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _build_prompt_refs() -> dict:
|
|
154
|
+
"""Build prompt references with the current date (not frozen at import)."""
|
|
155
|
+
return {
|
|
156
|
+
"RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(
|
|
157
|
+
date=datetime.now().strftime("%Y-%m-%d"),
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_base_kwargs(base_backend, base_middleware):
|
|
163
|
+
"""Build agent kwargs *without* MCP (fast, no subprocess spawning)."""
|
|
164
|
+
from .utils import load_subagents
|
|
165
|
+
from .tools import tavily_search, think_tool, skill_manager
|
|
166
|
+
|
|
167
|
+
tool_registry = {"think_tool": think_tool, "tavily_search": tavily_search}
|
|
168
|
+
base_tools = [think_tool, skill_manager]
|
|
169
|
+
|
|
170
|
+
subs = load_subagents(
|
|
171
|
+
SUBAGENTS_CONFIG,
|
|
172
|
+
tool_registry=tool_registry,
|
|
173
|
+
prompt_refs=_build_prompt_refs(),
|
|
174
|
+
)
|
|
175
|
+
_inject_subagent_middleware(subs)
|
|
176
|
+
return dict(
|
|
177
|
+
name="EvoScientist",
|
|
178
|
+
model=_ensure_chat_model(),
|
|
179
|
+
tools=list(base_tools),
|
|
180
|
+
backend=base_backend,
|
|
181
|
+
subagents=subs,
|
|
182
|
+
middleware=base_middleware,
|
|
183
|
+
system_prompt=_ensure_system_prompt(),
|
|
184
|
+
skills=["/skills/"],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_mcp_and_build_kwargs(base_backend, base_middleware):
|
|
189
|
+
"""Load MCP tools (cached by config) and build agent kwargs.
|
|
190
|
+
|
|
191
|
+
Re-connects to MCP servers only when the effective MCP config changes.
|
|
192
|
+
Falls back to base kwargs if no MCP configured.
|
|
193
|
+
"""
|
|
194
|
+
from .utils import load_subagents
|
|
195
|
+
from .tools import tavily_search, think_tool, skill_manager
|
|
196
|
+
|
|
197
|
+
mcp_by_agent = _load_mcp_tools_cached()
|
|
198
|
+
if not mcp_by_agent:
|
|
199
|
+
return _build_base_kwargs(base_backend, base_middleware)
|
|
200
|
+
|
|
201
|
+
tool_registry = {"think_tool": think_tool, "tavily_search": tavily_search}
|
|
202
|
+
base_tools = [think_tool, skill_manager]
|
|
203
|
+
|
|
204
|
+
# Fresh tool registry — start from base tools + MCP tools
|
|
205
|
+
registry = dict(tool_registry)
|
|
206
|
+
for tools in mcp_by_agent.values():
|
|
207
|
+
for t in tools:
|
|
208
|
+
registry[t.name] = t
|
|
209
|
+
|
|
210
|
+
mcp_main = mcp_by_agent.pop("main", [])
|
|
211
|
+
|
|
212
|
+
subs = load_subagents(
|
|
213
|
+
SUBAGENTS_CONFIG,
|
|
214
|
+
tool_registry=registry,
|
|
215
|
+
prompt_refs=_build_prompt_refs(),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
_inject_subagent_middleware(subs)
|
|
219
|
+
|
|
220
|
+
# Inject MCP tools into subagents by name
|
|
221
|
+
for sa in subs:
|
|
222
|
+
if sa_tools := mcp_by_agent.get(sa["name"], []):
|
|
223
|
+
sa.setdefault("tools", []).extend(sa_tools)
|
|
224
|
+
|
|
225
|
+
return dict(
|
|
226
|
+
name="EvoScientist",
|
|
227
|
+
model=_ensure_chat_model(),
|
|
228
|
+
tools=base_tools + mcp_main,
|
|
229
|
+
backend=base_backend,
|
|
230
|
+
subagents=subs,
|
|
231
|
+
middleware=base_middleware,
|
|
232
|
+
system_prompt=_ensure_system_prompt(),
|
|
233
|
+
skills=["/skills/"],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# =============================================================================
|
|
238
|
+
# Default agent (langgraph dev / notebooks)
|
|
239
|
+
# =============================================================================
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _get_default_backend():
|
|
243
|
+
"""Build the default composite backend from current paths."""
|
|
244
|
+
from deepagents.backends import FilesystemBackend, CompositeBackend
|
|
245
|
+
from .backends import CustomSandboxBackend, MergedReadOnlyBackend
|
|
246
|
+
|
|
247
|
+
workspace_dir = str(_paths_mod.WORKSPACE_ROOT)
|
|
248
|
+
set_active_workspace(workspace_dir)
|
|
249
|
+
memory_dir = str(_paths_mod.MEMORY_DIR)
|
|
250
|
+
user_skills_dir = str(_paths_mod.USER_SKILLS_DIR)
|
|
251
|
+
|
|
252
|
+
ws_backend = CustomSandboxBackend(
|
|
253
|
+
root_dir=workspace_dir,
|
|
254
|
+
virtual_mode=True,
|
|
255
|
+
timeout=300,
|
|
256
|
+
)
|
|
257
|
+
sk_backend = MergedReadOnlyBackend(
|
|
258
|
+
primary_dir=user_skills_dir,
|
|
259
|
+
secondary_dir=SKILLS_DIR,
|
|
260
|
+
)
|
|
261
|
+
mem_backend = FilesystemBackend(
|
|
262
|
+
root_dir=memory_dir,
|
|
263
|
+
virtual_mode=True,
|
|
264
|
+
)
|
|
265
|
+
return CompositeBackend(
|
|
266
|
+
default=ws_backend,
|
|
267
|
+
routes={
|
|
268
|
+
"/skills/": sk_backend,
|
|
269
|
+
"/memory/": mem_backend,
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _get_default_middleware():
|
|
275
|
+
"""Build the default middleware list."""
|
|
276
|
+
from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware
|
|
277
|
+
|
|
278
|
+
cfg = _ensure_config()
|
|
279
|
+
memory_dir = str(_paths_mod.MEMORY_DIR)
|
|
280
|
+
mw = [
|
|
281
|
+
ToolErrorHandlerMiddleware(),
|
|
282
|
+
create_memory_middleware(memory_dir, extraction_model=_ensure_chat_model()),
|
|
283
|
+
]
|
|
284
|
+
if cfg.enable_ask_user and not cfg.auto_approve:
|
|
285
|
+
from .middleware.ask_user import AskUserMiddleware
|
|
286
|
+
mw.insert(0, AskUserMiddleware())
|
|
287
|
+
return mw
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _get_default_agent():
|
|
291
|
+
"""Build the default agent (with MCP, no checkpointer) on first access."""
|
|
292
|
+
global _EvoScientist_agent
|
|
293
|
+
if _EvoScientist_agent is None:
|
|
294
|
+
from deepagents import create_deep_agent
|
|
295
|
+
|
|
296
|
+
be = _get_default_backend()
|
|
297
|
+
mw = _get_default_middleware()
|
|
298
|
+
kwargs = load_mcp_and_build_kwargs(be, mw)
|
|
299
|
+
_EvoScientist_agent = create_deep_agent(**kwargs).with_config(
|
|
300
|
+
{"recursion_limit": 1000}
|
|
301
|
+
)
|
|
302
|
+
return _EvoScientist_agent
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def __getattr__(name: str):
|
|
306
|
+
if name == "EvoScientist_agent":
|
|
307
|
+
return _get_default_agent()
|
|
308
|
+
# Backward compat for module-level names
|
|
309
|
+
if name == "chat_model":
|
|
310
|
+
return _ensure_chat_model()
|
|
311
|
+
if name == "SYSTEM_PROMPT":
|
|
312
|
+
return _ensure_system_prompt()
|
|
313
|
+
if name == "backend":
|
|
314
|
+
return _get_default_backend()
|
|
315
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# =============================================================================
|
|
319
|
+
# CLI agent factory
|
|
320
|
+
# =============================================================================
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config=None):
|
|
324
|
+
"""Create agent with checkpointer for CLI multi-turn support.
|
|
325
|
+
|
|
326
|
+
A fresh backend is constructed on every call using the current
|
|
327
|
+
``paths.WORKSPACE_ROOT`` (or the explicit *workspace_dir*), so
|
|
328
|
+
runtime ``set_workspace_root()`` changes are always respected.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
workspace_dir: Per-session workspace directory. If ``None``,
|
|
332
|
+
defaults to the current ``paths.WORKSPACE_ROOT``.
|
|
333
|
+
checkpointer: Optional LangGraph checkpointer. If ``None``,
|
|
334
|
+
falls back to ``InMemorySaver`` (non-persistent).
|
|
335
|
+
config: Optional pre-loaded ``EvoScientistConfig``. If ``None``,
|
|
336
|
+
loads from file/env/defaults. Passing this avoids double
|
|
337
|
+
loading when the CLI has already loaded config.
|
|
338
|
+
"""
|
|
339
|
+
import os as _os
|
|
340
|
+
|
|
341
|
+
from deepagents import create_deep_agent
|
|
342
|
+
from deepagents.backends import FilesystemBackend, CompositeBackend
|
|
343
|
+
from .backends import CustomSandboxBackend, MergedReadOnlyBackend
|
|
344
|
+
from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware
|
|
345
|
+
from . import paths as _paths
|
|
346
|
+
|
|
347
|
+
cfg = _ensure_config(config)
|
|
348
|
+
|
|
349
|
+
if checkpointer is None:
|
|
350
|
+
from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
|
|
351
|
+
checkpointer = InMemorySaver()
|
|
352
|
+
|
|
353
|
+
# When no explicit workspace_dir is provided, apply config.default_workdir
|
|
354
|
+
# as a fallback. This covers direct callers (notebooks, iMessage server)
|
|
355
|
+
# that never call set_workspace_root() themselves. CLI callers always
|
|
356
|
+
# pass workspace_dir explicitly, so their --workdir is never overwritten.
|
|
357
|
+
if workspace_dir is None:
|
|
358
|
+
if cfg.default_workdir:
|
|
359
|
+
set_workspace_root(
|
|
360
|
+
_os.path.abspath(_os.path.expanduser(cfg.default_workdir))
|
|
361
|
+
)
|
|
362
|
+
workspace_dir = str(_paths.WORKSPACE_ROOT)
|
|
363
|
+
|
|
364
|
+
# Read paths dynamically so runtime set_workspace_root() changes are picked up
|
|
365
|
+
_mem_dir = str(_paths.MEMORY_DIR)
|
|
366
|
+
_usr_skills_dir = str(_paths.USER_SKILLS_DIR)
|
|
367
|
+
|
|
368
|
+
# Always construct fresh backends from current paths (avoids stale
|
|
369
|
+
# module-level backend when workspace root changed at runtime).
|
|
370
|
+
set_active_workspace(workspace_dir)
|
|
371
|
+
ws_backend = CustomSandboxBackend(
|
|
372
|
+
root_dir=workspace_dir,
|
|
373
|
+
virtual_mode=True,
|
|
374
|
+
timeout=300,
|
|
375
|
+
)
|
|
376
|
+
sk_backend = MergedReadOnlyBackend(
|
|
377
|
+
primary_dir=_usr_skills_dir,
|
|
378
|
+
secondary_dir=SKILLS_DIR,
|
|
379
|
+
)
|
|
380
|
+
# Memory always uses SHARED directory (not per-session) for cross-session persistence
|
|
381
|
+
mem_backend = FilesystemBackend(
|
|
382
|
+
root_dir=_mem_dir,
|
|
383
|
+
virtual_mode=True,
|
|
384
|
+
)
|
|
385
|
+
be = CompositeBackend(
|
|
386
|
+
default=ws_backend,
|
|
387
|
+
routes={
|
|
388
|
+
"/skills/": sk_backend,
|
|
389
|
+
"/memory/": mem_backend,
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
mw = [
|
|
394
|
+
ToolErrorHandlerMiddleware(),
|
|
395
|
+
create_memory_middleware(_mem_dir, extraction_model=_ensure_chat_model()),
|
|
396
|
+
]
|
|
397
|
+
if cfg.enable_ask_user and not cfg.auto_approve:
|
|
398
|
+
from .middleware.ask_user import AskUserMiddleware
|
|
399
|
+
mw.insert(0, AskUserMiddleware())
|
|
400
|
+
|
|
401
|
+
# Re-load MCP tools from current config (picks up /mcp add changes)
|
|
402
|
+
kwargs = load_mcp_and_build_kwargs(be, mw)
|
|
403
|
+
|
|
404
|
+
# HITL: gate shell execution for user approval
|
|
405
|
+
_interrupt_on: dict[str, bool] | None = None
|
|
406
|
+
if not cfg.auto_approve:
|
|
407
|
+
_interrupt_on = {"execute": True}
|
|
408
|
+
|
|
409
|
+
return create_deep_agent(
|
|
410
|
+
**kwargs,
|
|
411
|
+
checkpointer=checkpointer,
|
|
412
|
+
interrupt_on=_interrupt_on,
|
|
413
|
+
).with_config({"recursion_limit": 1000})
|
|
@@ -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)
|