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.
Files changed (204) hide show
  1. evoscientist-0.0.1/EvoScientist/EvoScientist.py +413 -0
  2. evoscientist-0.0.1/EvoScientist/__init__.py +74 -0
  3. evoscientist-0.0.1/EvoScientist/__main__.py +4 -0
  4. evoscientist-0.0.1/EvoScientist/backends.py +425 -0
  5. evoscientist-0.0.1/EvoScientist/channels/__init__.py +44 -0
  6. evoscientist-0.0.1/EvoScientist/channels/base.py +1074 -0
  7. evoscientist-0.0.1/EvoScientist/channels/bus/__init__.py +6 -0
  8. evoscientist-0.0.1/EvoScientist/channels/bus/events.py +52 -0
  9. evoscientist-0.0.1/EvoScientist/channels/bus/message_bus.py +96 -0
  10. evoscientist-0.0.1/EvoScientist/channels/capabilities.py +220 -0
  11. evoscientist-0.0.1/EvoScientist/channels/channel_manager.py +1011 -0
  12. evoscientist-0.0.1/EvoScientist/channels/config.py +126 -0
  13. evoscientist-0.0.1/EvoScientist/channels/consumer.py +769 -0
  14. evoscientist-0.0.1/EvoScientist/channels/dingtalk/__init__.py +29 -0
  15. evoscientist-0.0.1/EvoScientist/channels/dingtalk/channel.py +354 -0
  16. evoscientist-0.0.1/EvoScientist/channels/dingtalk/probe.py +33 -0
  17. evoscientist-0.0.1/EvoScientist/channels/dingtalk/serve.py +92 -0
  18. evoscientist-0.0.1/EvoScientist/channels/discord/__init__.py +19 -0
  19. evoscientist-0.0.1/EvoScientist/channels/discord/channel.py +255 -0
  20. evoscientist-0.0.1/EvoScientist/channels/discord/probe.py +33 -0
  21. evoscientist-0.0.1/EvoScientist/channels/discord/serve.py +93 -0
  22. evoscientist-0.0.1/EvoScientist/channels/email/__init__.py +41 -0
  23. evoscientist-0.0.1/EvoScientist/channels/email/channel.py +380 -0
  24. evoscientist-0.0.1/EvoScientist/channels/email/probe.py +84 -0
  25. evoscientist-0.0.1/EvoScientist/channels/email/serve.py +124 -0
  26. evoscientist-0.0.1/EvoScientist/channels/feishu/__init__.py +22 -0
  27. evoscientist-0.0.1/EvoScientist/channels/feishu/channel.py +825 -0
  28. evoscientist-0.0.1/EvoScientist/channels/feishu/probe.py +39 -0
  29. evoscientist-0.0.1/EvoScientist/channels/feishu/serve.py +113 -0
  30. evoscientist-0.0.1/EvoScientist/channels/formatter.py +287 -0
  31. evoscientist-0.0.1/EvoScientist/channels/imessage/__init__.py +42 -0
  32. evoscientist-0.0.1/EvoScientist/channels/imessage/channel_rpc.py +407 -0
  33. evoscientist-0.0.1/EvoScientist/channels/imessage/probe.py +106 -0
  34. evoscientist-0.0.1/EvoScientist/channels/imessage/rpc_client.py +235 -0
  35. evoscientist-0.0.1/EvoScientist/channels/imessage/serve.py +87 -0
  36. evoscientist-0.0.1/EvoScientist/channels/imessage/targets.py +232 -0
  37. evoscientist-0.0.1/EvoScientist/channels/middleware.py +837 -0
  38. evoscientist-0.0.1/EvoScientist/channels/mixins.py +324 -0
  39. evoscientist-0.0.1/EvoScientist/channels/plugin.py +226 -0
  40. evoscientist-0.0.1/EvoScientist/channels/qq/__init__.py +26 -0
  41. evoscientist-0.0.1/EvoScientist/channels/qq/channel.py +259 -0
  42. evoscientist-0.0.1/EvoScientist/channels/qq/probe.py +37 -0
  43. evoscientist-0.0.1/EvoScientist/channels/qq/serve.py +87 -0
  44. evoscientist-0.0.1/EvoScientist/channels/retry.py +122 -0
  45. evoscientist-0.0.1/EvoScientist/channels/signal/__init__.py +27 -0
  46. evoscientist-0.0.1/EvoScientist/channels/signal/channel.py +462 -0
  47. evoscientist-0.0.1/EvoScientist/channels/signal/probe.py +39 -0
  48. evoscientist-0.0.1/EvoScientist/channels/signal/serve.py +99 -0
  49. evoscientist-0.0.1/EvoScientist/channels/slack/__init__.py +20 -0
  50. evoscientist-0.0.1/EvoScientist/channels/slack/channel.py +291 -0
  51. evoscientist-0.0.1/EvoScientist/channels/slack/probe.py +48 -0
  52. evoscientist-0.0.1/EvoScientist/channels/slack/serve.py +99 -0
  53. evoscientist-0.0.1/EvoScientist/channels/standalone.py +142 -0
  54. evoscientist-0.0.1/EvoScientist/channels/telegram/__init__.py +17 -0
  55. evoscientist-0.0.1/EvoScientist/channels/telegram/channel.py +289 -0
  56. evoscientist-0.0.1/EvoScientist/channels/telegram/probe.py +32 -0
  57. evoscientist-0.0.1/EvoScientist/channels/telegram/serve.py +81 -0
  58. evoscientist-0.0.1/EvoScientist/channels/wechat/__init__.py +69 -0
  59. evoscientist-0.0.1/EvoScientist/channels/wechat/channel.py +865 -0
  60. evoscientist-0.0.1/EvoScientist/channels/wechat/crypto.py +187 -0
  61. evoscientist-0.0.1/EvoScientist/channels/wechat/probe.py +72 -0
  62. evoscientist-0.0.1/EvoScientist/channels/wechat/serve.py +139 -0
  63. evoscientist-0.0.1/EvoScientist/channels/wechat/verify_server.py +175 -0
  64. evoscientist-0.0.1/EvoScientist/cli/__init__.py +37 -0
  65. evoscientist-0.0.1/EvoScientist/cli/_app.py +51 -0
  66. evoscientist-0.0.1/EvoScientist/cli/_constants.py +41 -0
  67. evoscientist-0.0.1/EvoScientist/cli/agent.py +66 -0
  68. evoscientist-0.0.1/EvoScientist/cli/channel.py +733 -0
  69. evoscientist-0.0.1/EvoScientist/cli/clipboard.py +116 -0
  70. evoscientist-0.0.1/EvoScientist/cli/commands.py +738 -0
  71. evoscientist-0.0.1/EvoScientist/cli/history_suggester.py +85 -0
  72. evoscientist-0.0.1/EvoScientist/cli/interactive.py +795 -0
  73. evoscientist-0.0.1/EvoScientist/cli/mcp_ui.py +282 -0
  74. evoscientist-0.0.1/EvoScientist/cli/skills_cmd.py +98 -0
  75. evoscientist-0.0.1/EvoScientist/cli/tui_backends.py +67 -0
  76. evoscientist-0.0.1/EvoScientist/cli/tui_interactive.py +2255 -0
  77. evoscientist-0.0.1/EvoScientist/cli/tui_runtime.py +107 -0
  78. evoscientist-0.0.1/EvoScientist/cli/widgets/__init__.py +31 -0
  79. evoscientist-0.0.1/EvoScientist/cli/widgets/approval_widget.py +211 -0
  80. evoscientist-0.0.1/EvoScientist/cli/widgets/ask_user_widget.py +375 -0
  81. evoscientist-0.0.1/EvoScientist/cli/widgets/assistant_message.py +59 -0
  82. evoscientist-0.0.1/EvoScientist/cli/widgets/loading_widget.py +50 -0
  83. evoscientist-0.0.1/EvoScientist/cli/widgets/subagent_widget.py +293 -0
  84. evoscientist-0.0.1/EvoScientist/cli/widgets/summarization_widget.py +92 -0
  85. evoscientist-0.0.1/EvoScientist/cli/widgets/system_message.py +20 -0
  86. evoscientist-0.0.1/EvoScientist/cli/widgets/thinking_widget.py +117 -0
  87. evoscientist-0.0.1/EvoScientist/cli/widgets/thread_selector.py +197 -0
  88. evoscientist-0.0.1/EvoScientist/cli/widgets/todo_widget.py +77 -0
  89. evoscientist-0.0.1/EvoScientist/cli/widgets/tool_call_widget.py +268 -0
  90. evoscientist-0.0.1/EvoScientist/cli/widgets/usage_widget.py +29 -0
  91. evoscientist-0.0.1/EvoScientist/cli/widgets/user_message.py +25 -0
  92. evoscientist-0.0.1/EvoScientist/config/__init__.py +47 -0
  93. evoscientist-0.0.1/EvoScientist/config/onboard.py +2157 -0
  94. evoscientist-0.0.1/EvoScientist/config/settings.py +420 -0
  95. evoscientist-0.0.1/EvoScientist/llm/__init__.py +23 -0
  96. evoscientist-0.0.1/EvoScientist/llm/models.py +251 -0
  97. evoscientist-0.0.1/EvoScientist/mcp/__init__.py +32 -0
  98. evoscientist-0.0.1/EvoScientist/mcp/client.py +695 -0
  99. evoscientist-0.0.1/EvoScientist/middleware/__init__.py +33 -0
  100. evoscientist-0.0.1/EvoScientist/middleware/ask_user.py +416 -0
  101. evoscientist-0.0.1/EvoScientist/middleware/memory.py +786 -0
  102. evoscientist-0.0.1/EvoScientist/middleware/tool_error_handler.py +80 -0
  103. evoscientist-0.0.1/EvoScientist/paths.py +81 -0
  104. evoscientist-0.0.1/EvoScientist/prompts.py +339 -0
  105. evoscientist-0.0.1/EvoScientist/sessions.py +345 -0
  106. evoscientist-0.0.1/EvoScientist/skills/find-skills/SKILL.md +76 -0
  107. evoscientist-0.0.1/EvoScientist/skills/skill-creator/LICENSE.txt +205 -0
  108. evoscientist-0.0.1/EvoScientist/skills/skill-creator/SKILL.md +437 -0
  109. evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/analyzer.md +274 -0
  110. evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/comparator.md +202 -0
  111. evoscientist-0.0.1/EvoScientist/skills/skill-creator/agents/grader.md +223 -0
  112. evoscientist-0.0.1/EvoScientist/skills/skill-creator/assets/eval_review.html +147 -0
  113. evoscientist-0.0.1/EvoScientist/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  114. evoscientist-0.0.1/EvoScientist/skills/skill-creator/eval-viewer/viewer.html +1335 -0
  115. evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
  116. evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/schemas.md +430 -0
  117. evoscientist-0.0.1/EvoScientist/skills/skill-creator/references/workflows.md +28 -0
  118. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__init__.py +0 -0
  119. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  120. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-311.pyc +0 -0
  121. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/generate_report.cpython-311.pyc +0 -0
  122. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/improve_description.cpython-311.pyc +0 -0
  123. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/init_skill.cpython-311.pyc +0 -0
  124. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/package_skill.cpython-311.pyc +0 -0
  125. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
  126. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/run_eval.cpython-311.pyc +0 -0
  127. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/run_loop.cpython-311.pyc +0 -0
  128. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/__pycache__/utils.cpython-311.pyc +0 -0
  129. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/aggregate_benchmark.py +416 -0
  130. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/generate_report.py +320 -0
  131. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/improve_description.py +285 -0
  132. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
  133. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/package_skill.py +139 -0
  134. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/quick_validate.py +119 -0
  135. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/run_eval.py +241 -0
  136. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/run_loop.py +335 -0
  137. evoscientist-0.0.1/EvoScientist/skills/skill-creator/scripts/utils.py +47 -0
  138. evoscientist-0.0.1/EvoScientist/stream/__init__.py +84 -0
  139. evoscientist-0.0.1/EvoScientist/stream/diff_format.py +207 -0
  140. evoscientist-0.0.1/EvoScientist/stream/display.py +1271 -0
  141. evoscientist-0.0.1/EvoScientist/stream/emitter.py +132 -0
  142. evoscientist-0.0.1/EvoScientist/stream/events.py +592 -0
  143. evoscientist-0.0.1/EvoScientist/stream/formatter.py +168 -0
  144. evoscientist-0.0.1/EvoScientist/stream/state.py +384 -0
  145. evoscientist-0.0.1/EvoScientist/stream/tracker.py +115 -0
  146. evoscientist-0.0.1/EvoScientist/stream/utils.py +266 -0
  147. evoscientist-0.0.1/EvoScientist/subagent.yaml +160 -0
  148. evoscientist-0.0.1/EvoScientist/tools/__init__.py +16 -0
  149. evoscientist-0.0.1/EvoScientist/tools/search.py +115 -0
  150. evoscientist-0.0.1/EvoScientist/tools/skill_manager.py +120 -0
  151. evoscientist-0.0.1/EvoScientist/tools/skills_manager.py +504 -0
  152. evoscientist-0.0.1/EvoScientist/tools/think.py +58 -0
  153. evoscientist-0.0.1/EvoScientist/utils.py +217 -0
  154. evoscientist-0.0.1/EvoScientist.egg-info/PKG-INFO +565 -0
  155. evoscientist-0.0.1/EvoScientist.egg-info/SOURCES.txt +202 -0
  156. evoscientist-0.0.1/EvoScientist.egg-info/dependency_links.txt +1 -0
  157. evoscientist-0.0.1/EvoScientist.egg-info/entry_points.txt +5 -0
  158. evoscientist-0.0.1/EvoScientist.egg-info/requires.txt +45 -0
  159. evoscientist-0.0.1/EvoScientist.egg-info/top_level.txt +1 -0
  160. evoscientist-0.0.1/LICENSE +201 -0
  161. evoscientist-0.0.1/PKG-INFO +565 -0
  162. evoscientist-0.0.1/README.md +509 -0
  163. evoscientist-0.0.1/pyproject.toml +91 -0
  164. evoscientist-0.0.1/setup.cfg +4 -0
  165. evoscientist-0.0.1/tests/test_additional_channel_smoke.py +96 -0
  166. evoscientist-0.0.1/tests/test_agent_mcp_cache.py +59 -0
  167. evoscientist-0.0.1/tests/test_ask_user.py +514 -0
  168. evoscientist-0.0.1/tests/test_backends.py +397 -0
  169. evoscientist-0.0.1/tests/test_bus_integration.py +272 -0
  170. evoscientist-0.0.1/tests/test_channel_comprehensive.py +1621 -0
  171. evoscientist-0.0.1/tests/test_cli_channel_bus_mode.py +122 -0
  172. evoscientist-0.0.1/tests/test_cli_run_name.py +38 -0
  173. evoscientist-0.0.1/tests/test_cli_serve.py +144 -0
  174. evoscientist-0.0.1/tests/test_cli_tui_dispatch.py +53 -0
  175. evoscientist-0.0.1/tests/test_config.py +383 -0
  176. evoscientist-0.0.1/tests/test_diff_format.py +246 -0
  177. evoscientist-0.0.1/tests/test_dingtalk_channel.py +290 -0
  178. evoscientist-0.0.1/tests/test_discord_channel.py +63 -0
  179. evoscientist-0.0.1/tests/test_event_loop.py +200 -0
  180. evoscientist-0.0.1/tests/test_feishu_channel.py +497 -0
  181. evoscientist-0.0.1/tests/test_hitl.py +532 -0
  182. evoscientist-0.0.1/tests/test_llm.py +464 -0
  183. evoscientist-0.0.1/tests/test_mcp_client.py +691 -0
  184. evoscientist-0.0.1/tests/test_memory_merge.py +73 -0
  185. evoscientist-0.0.1/tests/test_onboard.py +818 -0
  186. evoscientist-0.0.1/tests/test_paths.py +105 -0
  187. evoscientist-0.0.1/tests/test_prompts.py +37 -0
  188. evoscientist-0.0.1/tests/test_rich_escape.py +18 -0
  189. evoscientist-0.0.1/tests/test_sessions.py +301 -0
  190. evoscientist-0.0.1/tests/test_skills_manager.py +415 -0
  191. evoscientist-0.0.1/tests/test_slack_channel.py +79 -0
  192. evoscientist-0.0.1/tests/test_stream_emitter.py +105 -0
  193. evoscientist-0.0.1/tests/test_stream_events.py +215 -0
  194. evoscientist-0.0.1/tests/test_stream_state.py +595 -0
  195. evoscientist-0.0.1/tests/test_stream_tracker.py +99 -0
  196. evoscientist-0.0.1/tests/test_stream_utils.py +232 -0
  197. evoscientist-0.0.1/tests/test_summarization.py +169 -0
  198. evoscientist-0.0.1/tests/test_telegram_channel.py +60 -0
  199. evoscientist-0.0.1/tests/test_thread_selector.py +204 -0
  200. evoscientist-0.0.1/tests/test_tool_error_handler.py +220 -0
  201. evoscientist-0.0.1/tests/test_tools.py +22 -0
  202. evoscientist-0.0.1/tests/test_tui_widgets.py +451 -0
  203. evoscientist-0.0.1/tests/test_ui_runtime.py +65 -0
  204. 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)
@@ -0,0 +1,4 @@
1
+ """Enable `python -m EvoScientist` execution."""
2
+ from EvoScientist.cli import main
3
+
4
+ main()