EvoScientist 0.0.1.dev2__py3-none-any.whl → 0.0.1.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- EvoScientist/EvoScientist.py +58 -22
- EvoScientist/__init__.py +19 -0
- EvoScientist/cli.py +480 -1365
- EvoScientist/config.py +274 -0
- EvoScientist/llm/__init__.py +21 -0
- EvoScientist/llm/models.py +99 -0
- EvoScientist/memory.py +715 -0
- EvoScientist/middleware.py +49 -4
- EvoScientist/onboard.py +725 -0
- EvoScientist/paths.py +44 -0
- EvoScientist/skills_manager.py +391 -0
- EvoScientist/stream/__init__.py +25 -0
- EvoScientist/stream/display.py +604 -0
- EvoScientist/stream/events.py +415 -0
- EvoScientist/stream/state.py +343 -0
- EvoScientist/stream/utils.py +23 -16
- EvoScientist/tools.py +75 -2
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/METADATA +144 -4
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/RECORD +23 -13
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/WHEEL +0 -0
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/entry_points.txt +0 -0
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/licenses/LICENSE +0 -0
- {evoscientist-0.0.1.dev2.dist-info → evoscientist-0.0.1.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Stream state tracking for CLI display.
|
|
2
|
+
|
|
3
|
+
Contains SubAgentState, StreamState, and todo-item parsing helpers.
|
|
4
|
+
No Rich dependencies — stdlib only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubAgentState:
|
|
12
|
+
"""Tracks a single sub-agent's activity."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str, description: str = ""):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.description = description
|
|
17
|
+
self.tool_calls: list[dict] = []
|
|
18
|
+
self.tool_results: list[dict] = []
|
|
19
|
+
self._result_map: dict[str, dict] = {} # tool_call_id -> result
|
|
20
|
+
self.is_active = True
|
|
21
|
+
|
|
22
|
+
def add_tool_call(self, name: str, args: dict, tool_id: str = ""):
|
|
23
|
+
# Skip empty-name calls without an id (incomplete streaming chunks)
|
|
24
|
+
if not name and not tool_id:
|
|
25
|
+
return
|
|
26
|
+
tc_data = {"id": tool_id, "name": name, "args": args}
|
|
27
|
+
if tool_id:
|
|
28
|
+
for i, tc in enumerate(self.tool_calls):
|
|
29
|
+
if tc.get("id") == tool_id:
|
|
30
|
+
# Merge: keep the non-empty name/args
|
|
31
|
+
if name:
|
|
32
|
+
self.tool_calls[i]["name"] = name
|
|
33
|
+
if args:
|
|
34
|
+
self.tool_calls[i]["args"] = args
|
|
35
|
+
return
|
|
36
|
+
# Skip if name is empty and we can't deduplicate by id
|
|
37
|
+
if not name:
|
|
38
|
+
return
|
|
39
|
+
self.tool_calls.append(tc_data)
|
|
40
|
+
|
|
41
|
+
def add_tool_result(self, name: str, content: str, success: bool = True):
|
|
42
|
+
result = {"name": name, "content": content, "success": success}
|
|
43
|
+
self.tool_results.append(result)
|
|
44
|
+
# Try to match result to the first unmatched tool call with same name
|
|
45
|
+
for tc in self.tool_calls:
|
|
46
|
+
tc_id = tc.get("id", "")
|
|
47
|
+
tc_name = tc.get("name", "")
|
|
48
|
+
if tc_id and tc_id not in self._result_map and tc_name == name:
|
|
49
|
+
self._result_map[tc_id] = result
|
|
50
|
+
return
|
|
51
|
+
# Fallback: match first unmatched tool call
|
|
52
|
+
for tc in self.tool_calls:
|
|
53
|
+
tc_id = tc.get("id", "")
|
|
54
|
+
if tc_id and tc_id not in self._result_map:
|
|
55
|
+
self._result_map[tc_id] = result
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
def get_result_for(self, tc: dict) -> dict | None:
|
|
59
|
+
"""Get matched result for a tool call."""
|
|
60
|
+
tc_id = tc.get("id", "")
|
|
61
|
+
if tc_id:
|
|
62
|
+
return self._result_map.get(tc_id)
|
|
63
|
+
# Fallback: index-based matching
|
|
64
|
+
try:
|
|
65
|
+
idx = self.tool_calls.index(tc)
|
|
66
|
+
if idx < len(self.tool_results):
|
|
67
|
+
return self.tool_results[idx]
|
|
68
|
+
except ValueError:
|
|
69
|
+
pass
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StreamState:
|
|
74
|
+
"""Accumulates stream state for display updates."""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self.thinking_text = ""
|
|
78
|
+
self.response_text = ""
|
|
79
|
+
self.tool_calls = []
|
|
80
|
+
self.tool_results = []
|
|
81
|
+
self.is_thinking = False
|
|
82
|
+
self.is_responding = False
|
|
83
|
+
self.is_processing = False
|
|
84
|
+
# Sub-agent tracking
|
|
85
|
+
self.subagents: list[SubAgentState] = []
|
|
86
|
+
self._subagent_map: dict[str, SubAgentState] = {} # name -> state
|
|
87
|
+
# Todo list tracking
|
|
88
|
+
self.todo_items: list[dict] = []
|
|
89
|
+
# Latest text segment (reset on each tool_call)
|
|
90
|
+
self.latest_text = ""
|
|
91
|
+
|
|
92
|
+
def _get_or_create_subagent(self, name: str, description: str = "") -> SubAgentState:
|
|
93
|
+
if name not in self._subagent_map:
|
|
94
|
+
# Case 1: real name arrives, "sub-agent" entry exists -> rename it
|
|
95
|
+
if name != "sub-agent" and "sub-agent" in self._subagent_map:
|
|
96
|
+
old_sa = self._subagent_map.pop("sub-agent")
|
|
97
|
+
old_sa.name = name
|
|
98
|
+
if description:
|
|
99
|
+
old_sa.description = description
|
|
100
|
+
self._subagent_map[name] = old_sa
|
|
101
|
+
return old_sa
|
|
102
|
+
# Case 2: "sub-agent" arrives but a pre-registered real-name entry
|
|
103
|
+
# exists with no tool calls -> merge into it
|
|
104
|
+
if name == "sub-agent":
|
|
105
|
+
active_named = [
|
|
106
|
+
sa for sa in self.subagents
|
|
107
|
+
if sa.is_active and sa.name != "sub-agent"
|
|
108
|
+
]
|
|
109
|
+
if len(active_named) == 1 and not active_named[0].tool_calls:
|
|
110
|
+
self._subagent_map[name] = active_named[0]
|
|
111
|
+
return active_named[0]
|
|
112
|
+
sa = SubAgentState(name, description)
|
|
113
|
+
self.subagents.append(sa)
|
|
114
|
+
self._subagent_map[name] = sa
|
|
115
|
+
else:
|
|
116
|
+
existing = self._subagent_map[name]
|
|
117
|
+
if description and not existing.description:
|
|
118
|
+
existing.description = description
|
|
119
|
+
# If this entry was created as "sub-agent" placeholder and the
|
|
120
|
+
# actual name is different, update.
|
|
121
|
+
if name != "sub-agent" and existing.name == "sub-agent":
|
|
122
|
+
existing.name = name
|
|
123
|
+
return self._subagent_map[name]
|
|
124
|
+
|
|
125
|
+
def _resolve_subagent_name(self, name: str) -> str:
|
|
126
|
+
"""Resolve "sub-agent" to the single active named sub-agent when possible."""
|
|
127
|
+
if name != "sub-agent":
|
|
128
|
+
return name
|
|
129
|
+
active_named = [
|
|
130
|
+
sa.name for sa in self.subagents
|
|
131
|
+
if sa.is_active and sa.name != "sub-agent"
|
|
132
|
+
]
|
|
133
|
+
if len(active_named) == 1:
|
|
134
|
+
return active_named[0]
|
|
135
|
+
return name
|
|
136
|
+
|
|
137
|
+
def handle_event(self, event: dict) -> str:
|
|
138
|
+
"""Process a single stream event, update internal state, return event type."""
|
|
139
|
+
event_type: str = event.get("type", "")
|
|
140
|
+
|
|
141
|
+
if event_type == "thinking":
|
|
142
|
+
self.is_thinking = True
|
|
143
|
+
self.is_responding = False
|
|
144
|
+
self.is_processing = False
|
|
145
|
+
self.thinking_text += event.get("content", "")
|
|
146
|
+
|
|
147
|
+
elif event_type == "text":
|
|
148
|
+
self.is_thinking = False
|
|
149
|
+
self.is_responding = True
|
|
150
|
+
self.is_processing = False
|
|
151
|
+
text_content = event.get("content", "")
|
|
152
|
+
self.response_text += text_content
|
|
153
|
+
self.latest_text += text_content
|
|
154
|
+
|
|
155
|
+
elif event_type == "tool_call":
|
|
156
|
+
self.is_thinking = False
|
|
157
|
+
self.is_responding = False
|
|
158
|
+
self.is_processing = False
|
|
159
|
+
self.latest_text = "" # Reset -- next text segment is a new message
|
|
160
|
+
|
|
161
|
+
tool_id = event.get("id", "")
|
|
162
|
+
tool_name = event.get("name", "unknown")
|
|
163
|
+
tool_args = event.get("args", {})
|
|
164
|
+
tc_data = {
|
|
165
|
+
"id": tool_id,
|
|
166
|
+
"name": tool_name,
|
|
167
|
+
"args": tool_args,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if tool_id:
|
|
171
|
+
updated = False
|
|
172
|
+
for i, tc in enumerate(self.tool_calls):
|
|
173
|
+
if tc.get("id") == tool_id:
|
|
174
|
+
self.tool_calls[i] = tc_data
|
|
175
|
+
updated = True
|
|
176
|
+
break
|
|
177
|
+
if not updated:
|
|
178
|
+
self.tool_calls.append(tc_data)
|
|
179
|
+
else:
|
|
180
|
+
self.tool_calls.append(tc_data)
|
|
181
|
+
|
|
182
|
+
# Capture todo items from write_todos args (most reliable source)
|
|
183
|
+
if tool_name == "write_todos":
|
|
184
|
+
todos = tool_args.get("todos", [])
|
|
185
|
+
if isinstance(todos, list) and todos:
|
|
186
|
+
self.todo_items = todos
|
|
187
|
+
|
|
188
|
+
elif event_type == "tool_result":
|
|
189
|
+
self.is_processing = True
|
|
190
|
+
result_name = event.get("name", "unknown")
|
|
191
|
+
result_content = event.get("content", "")
|
|
192
|
+
self.tool_results.append({
|
|
193
|
+
"name": result_name,
|
|
194
|
+
"content": result_content,
|
|
195
|
+
})
|
|
196
|
+
# Update todo list from write_todos / read_todos results (fallback)
|
|
197
|
+
if result_name in ("write_todos", "read_todos"):
|
|
198
|
+
parsed = _parse_todo_items(result_content)
|
|
199
|
+
if parsed:
|
|
200
|
+
self.todo_items = parsed
|
|
201
|
+
|
|
202
|
+
elif event_type == "subagent_start":
|
|
203
|
+
name = event.get("name", "sub-agent")
|
|
204
|
+
desc = event.get("description", "")
|
|
205
|
+
sa = self._get_or_create_subagent(name, desc)
|
|
206
|
+
sa.is_active = True
|
|
207
|
+
|
|
208
|
+
elif event_type == "subagent_tool_call":
|
|
209
|
+
sa_name = self._resolve_subagent_name(event.get("subagent", "sub-agent"))
|
|
210
|
+
sa = self._get_or_create_subagent(sa_name)
|
|
211
|
+
sa.add_tool_call(
|
|
212
|
+
event.get("name", "unknown"),
|
|
213
|
+
event.get("args", {}),
|
|
214
|
+
event.get("id", ""),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
elif event_type == "subagent_tool_result":
|
|
218
|
+
sa_name = self._resolve_subagent_name(event.get("subagent", "sub-agent"))
|
|
219
|
+
sa = self._get_or_create_subagent(sa_name)
|
|
220
|
+
sa.add_tool_result(
|
|
221
|
+
event.get("name", "unknown"),
|
|
222
|
+
event.get("content", ""),
|
|
223
|
+
event.get("success", True),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
elif event_type == "subagent_end":
|
|
227
|
+
name = self._resolve_subagent_name(event.get("name", "sub-agent"))
|
|
228
|
+
if name in self._subagent_map:
|
|
229
|
+
self._subagent_map[name].is_active = False
|
|
230
|
+
elif name == "sub-agent":
|
|
231
|
+
# Couldn't resolve -- deactivate the oldest active sub-agent
|
|
232
|
+
for sa in self.subagents:
|
|
233
|
+
if sa.is_active:
|
|
234
|
+
sa.is_active = False
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
elif event_type == "done":
|
|
238
|
+
self.is_processing = False
|
|
239
|
+
if not self.response_text:
|
|
240
|
+
self.response_text = event.get("response", "")
|
|
241
|
+
|
|
242
|
+
elif event_type == "error":
|
|
243
|
+
self.is_processing = False
|
|
244
|
+
self.is_thinking = False
|
|
245
|
+
self.is_responding = False
|
|
246
|
+
error_msg = event.get("message", "Unknown error")
|
|
247
|
+
self.response_text += f"\n\n[Error] {error_msg}"
|
|
248
|
+
|
|
249
|
+
return event_type
|
|
250
|
+
|
|
251
|
+
def get_display_args(self) -> dict:
|
|
252
|
+
"""Get kwargs for create_streaming_display()."""
|
|
253
|
+
return {
|
|
254
|
+
"thinking_text": self.thinking_text,
|
|
255
|
+
"response_text": self.response_text,
|
|
256
|
+
"latest_text": self.latest_text,
|
|
257
|
+
"tool_calls": self.tool_calls,
|
|
258
|
+
"tool_results": self.tool_results,
|
|
259
|
+
"is_thinking": self.is_thinking,
|
|
260
|
+
"is_responding": self.is_responding,
|
|
261
|
+
"is_processing": self.is_processing,
|
|
262
|
+
"subagents": self.subagents,
|
|
263
|
+
"todo_items": self.todo_items,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_todo_items(content: str) -> list[dict] | None:
|
|
268
|
+
"""Parse todo items from write_todos output.
|
|
269
|
+
|
|
270
|
+
Attempts to extract a list of dicts with 'status' and 'content' keys
|
|
271
|
+
from the tool result string. Returns None if parsing fails.
|
|
272
|
+
|
|
273
|
+
Handles formats like:
|
|
274
|
+
- Raw JSON/Python list: [{"content": "...", "status": "..."}]
|
|
275
|
+
- Prefixed: "Updated todo list to [{'content': '...', ...}]"
|
|
276
|
+
"""
|
|
277
|
+
content = content.strip()
|
|
278
|
+
|
|
279
|
+
def _try_parse(text: str) -> list[dict] | None:
|
|
280
|
+
"""Try JSON then Python literal parsing."""
|
|
281
|
+
text = text.strip()
|
|
282
|
+
try:
|
|
283
|
+
data = json.loads(text)
|
|
284
|
+
if isinstance(data, list) and data and isinstance(data[0], dict):
|
|
285
|
+
return data
|
|
286
|
+
except (json.JSONDecodeError, ValueError):
|
|
287
|
+
pass
|
|
288
|
+
try:
|
|
289
|
+
data = ast.literal_eval(text)
|
|
290
|
+
if isinstance(data, list) and data and isinstance(data[0], dict):
|
|
291
|
+
return data
|
|
292
|
+
except (ValueError, SyntaxError):
|
|
293
|
+
pass
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
# Try the full content directly
|
|
297
|
+
result = _try_parse(content)
|
|
298
|
+
if result:
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
# Extract embedded [...] from content (e.g. "Updated todo list to [{...}]")
|
|
302
|
+
bracket_start = content.find("[")
|
|
303
|
+
if bracket_start != -1:
|
|
304
|
+
bracket_end = content.rfind("]")
|
|
305
|
+
if bracket_end > bracket_start:
|
|
306
|
+
embedded = content[bracket_start:bracket_end + 1]
|
|
307
|
+
result = _try_parse(embedded)
|
|
308
|
+
if result:
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
# Try line-by-line scan
|
|
312
|
+
for line in content.split("\n"):
|
|
313
|
+
line = line.strip()
|
|
314
|
+
if "[" in line:
|
|
315
|
+
start = line.find("[")
|
|
316
|
+
end = line.rfind("]")
|
|
317
|
+
if end > start:
|
|
318
|
+
result = _try_parse(line[start:end + 1])
|
|
319
|
+
if result:
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _build_todo_stats(items: list[dict]) -> str:
|
|
326
|
+
"""Build stats string like '2 active | 1 pending | 3 done'."""
|
|
327
|
+
counts: dict[str, int] = {}
|
|
328
|
+
for item in items:
|
|
329
|
+
status = str(item.get("status", "todo")).lower()
|
|
330
|
+
# Normalize status names
|
|
331
|
+
if status in ("done", "completed", "complete"):
|
|
332
|
+
status = "done"
|
|
333
|
+
elif status in ("active", "in_progress", "in-progress", "working"):
|
|
334
|
+
status = "active"
|
|
335
|
+
else:
|
|
336
|
+
status = "pending"
|
|
337
|
+
counts[status] = counts.get(status, 0) + 1
|
|
338
|
+
|
|
339
|
+
parts = []
|
|
340
|
+
for key in ("active", "pending", "done"):
|
|
341
|
+
if counts.get(key, 0) > 0:
|
|
342
|
+
parts.append(f"{counts[key]} {key}")
|
|
343
|
+
return " | ".join(parts) if parts else f"{len(items)} items"
|
EvoScientist/stream/utils.py
CHANGED
|
@@ -114,34 +114,40 @@ def format_tool_compact(name: str, args: dict | None) -> str:
|
|
|
114
114
|
if name_lower == "execute":
|
|
115
115
|
cmd = args.get("command", "")
|
|
116
116
|
if len(cmd) > 50:
|
|
117
|
-
cmd = cmd[:47] + "
|
|
117
|
+
cmd = cmd[:47] + "\u2026"
|
|
118
118
|
return f"execute({cmd})"
|
|
119
119
|
|
|
120
|
-
# File operations
|
|
120
|
+
# File operations (with special case for memory files)
|
|
121
121
|
if name_lower == "read_file":
|
|
122
|
-
path =
|
|
123
|
-
|
|
122
|
+
path = args.get("path", "")
|
|
123
|
+
if path.endswith("/MEMORY.md") or path == "/MEMORY.md":
|
|
124
|
+
return "Reading memory"
|
|
125
|
+
return f"read_file({_shorten_path(path)})"
|
|
124
126
|
|
|
125
127
|
if name_lower == "write_file":
|
|
126
|
-
path =
|
|
127
|
-
|
|
128
|
+
path = args.get("path", "")
|
|
129
|
+
if path.endswith("/MEMORY.md") or path == "/MEMORY.md":
|
|
130
|
+
return "Updating memory"
|
|
131
|
+
return f"write_file({_shorten_path(path)})"
|
|
128
132
|
|
|
129
133
|
if name_lower == "edit_file":
|
|
130
|
-
path =
|
|
131
|
-
|
|
134
|
+
path = args.get("path", "")
|
|
135
|
+
if path.endswith("/MEMORY.md") or path == "/MEMORY.md":
|
|
136
|
+
return "Updating memory"
|
|
137
|
+
return f"edit_file({_shorten_path(path)})"
|
|
132
138
|
|
|
133
139
|
# Search operations
|
|
134
140
|
if name_lower == "glob":
|
|
135
141
|
pattern = args.get("pattern", "")
|
|
136
142
|
if len(pattern) > 40:
|
|
137
|
-
pattern = pattern[:37] + "
|
|
143
|
+
pattern = pattern[:37] + "\u2026"
|
|
138
144
|
return f"glob({pattern})"
|
|
139
145
|
|
|
140
146
|
if name_lower == "grep":
|
|
141
147
|
pattern = args.get("pattern", "")
|
|
142
148
|
path = args.get("path", ".")
|
|
143
149
|
if len(pattern) > 30:
|
|
144
|
-
pattern = pattern[:27] + "
|
|
150
|
+
pattern = pattern[:27] + "\u2026"
|
|
145
151
|
return f"grep({pattern}, {path})"
|
|
146
152
|
|
|
147
153
|
# Directory listing
|
|
@@ -163,16 +169,17 @@ def format_tool_compact(name: str, args: dict | None) -> str:
|
|
|
163
169
|
if name_lower == "task":
|
|
164
170
|
sa_type = args.get("subagent_type", "").strip()
|
|
165
171
|
task_desc = args.get("description", args.get("task", "")).strip()
|
|
172
|
+
task_desc = task_desc.split("\n")[0].strip() if task_desc else ""
|
|
166
173
|
if sa_type:
|
|
167
174
|
if task_desc:
|
|
168
175
|
if len(task_desc) > 50:
|
|
169
|
-
task_desc = task_desc[:47] + "
|
|
176
|
+
task_desc = task_desc[:47] + "\u2026"
|
|
170
177
|
return f"Cooking with {sa_type} — {task_desc}"
|
|
171
178
|
return f"Cooking with {sa_type}"
|
|
172
179
|
# Fallback if no subagent_type
|
|
173
180
|
if task_desc:
|
|
174
181
|
if len(task_desc) > 50:
|
|
175
|
-
task_desc = task_desc[:47] + "
|
|
182
|
+
task_desc = task_desc[:47] + "\u2026"
|
|
176
183
|
return f"Cooking with sub-agent — {task_desc}"
|
|
177
184
|
return "Cooking with sub-agent"
|
|
178
185
|
|
|
@@ -185,14 +192,14 @@ def format_tool_compact(name: str, args: dict | None) -> str:
|
|
|
185
192
|
if name_lower in ("tavily_search", "internet_search"):
|
|
186
193
|
query = args.get("query", "")
|
|
187
194
|
if len(query) > 40:
|
|
188
|
-
query = query[:37] + "
|
|
195
|
+
query = query[:37] + "\u2026"
|
|
189
196
|
return f"{name}({query})"
|
|
190
197
|
|
|
191
198
|
# Think/reflection
|
|
192
199
|
if name_lower == "think_tool":
|
|
193
200
|
reflection = args.get("reflection", "")
|
|
194
201
|
if len(reflection) > 40:
|
|
195
|
-
reflection = reflection[:37] + "
|
|
202
|
+
reflection = reflection[:37] + "\u2026"
|
|
196
203
|
return f"think_tool({reflection})"
|
|
197
204
|
|
|
198
205
|
# Default: show first few params
|
|
@@ -200,12 +207,12 @@ def format_tool_compact(name: str, args: dict | None) -> str:
|
|
|
200
207
|
for k, v in list(args.items())[:2]:
|
|
201
208
|
v_str = str(v)
|
|
202
209
|
if len(v_str) > 20:
|
|
203
|
-
v_str = v_str[:17] + "
|
|
210
|
+
v_str = v_str[:17] + "\u2026"
|
|
204
211
|
params.append(f"{k}={v_str}")
|
|
205
212
|
|
|
206
213
|
params_str = ", ".join(params)
|
|
207
214
|
if len(params_str) > 50:
|
|
208
|
-
params_str = params_str[:47] + "
|
|
215
|
+
params_str = params_str[:47] + "\u2026"
|
|
209
216
|
|
|
210
217
|
return f"{name}({params_str})"
|
|
211
218
|
|
EvoScientist/tools.py
CHANGED
|
@@ -16,7 +16,16 @@ from typing_extensions import Annotated
|
|
|
16
16
|
|
|
17
17
|
load_dotenv(override=True)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Lazy initialization - only create client when needed
|
|
20
|
+
_tavily_client = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_tavily_client() -> TavilyClient:
|
|
24
|
+
"""Get or create the Tavily client (lazy initialization)."""
|
|
25
|
+
global _tavily_client
|
|
26
|
+
if _tavily_client is None:
|
|
27
|
+
_tavily_client = TavilyClient()
|
|
28
|
+
return _tavily_client
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
async def fetch_webpage_content(url: str, timeout: float = 10.0) -> str:
|
|
@@ -67,7 +76,7 @@ async def tavily_search(
|
|
|
67
76
|
"""
|
|
68
77
|
|
|
69
78
|
def _sync_search() -> dict:
|
|
70
|
-
return
|
|
79
|
+
return _get_tavily_client().search(
|
|
71
80
|
query,
|
|
72
81
|
max_results=max_results,
|
|
73
82
|
topic=topic,
|
|
@@ -106,6 +115,70 @@ async def tavily_search(
|
|
|
106
115
|
return f"Search failed: {str(e)}"
|
|
107
116
|
|
|
108
117
|
|
|
118
|
+
@tool(parse_docstring=True)
|
|
119
|
+
def skill_manager(
|
|
120
|
+
action: Literal["install", "list", "uninstall"],
|
|
121
|
+
source: str = "",
|
|
122
|
+
name: str = "",
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Manage user skills: install, list, or uninstall.
|
|
125
|
+
|
|
126
|
+
Use this tool when the user asks to:
|
|
127
|
+
- Install a skill (action="install", source required)
|
|
128
|
+
- List installed skills (action="list")
|
|
129
|
+
- Uninstall a skill (action="uninstall", name required)
|
|
130
|
+
|
|
131
|
+
Supported sources for install:
|
|
132
|
+
- Local path: "./my-skill" or "/path/to/skill"
|
|
133
|
+
- GitHub URL: "https://github.com/owner/repo/tree/main/skill-name"
|
|
134
|
+
- GitHub shorthand: "owner/repo@skill-name"
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
action: One of "install", "list", or "uninstall"
|
|
138
|
+
source: For install - local path or GitHub URL/shorthand
|
|
139
|
+
name: For uninstall - skill name to remove
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Result message
|
|
143
|
+
"""
|
|
144
|
+
from .skills_manager import install_skill, list_skills, uninstall_skill
|
|
145
|
+
|
|
146
|
+
if action == "install":
|
|
147
|
+
if not source:
|
|
148
|
+
return "Error: 'source' is required for install action"
|
|
149
|
+
result = install_skill(source)
|
|
150
|
+
if result["success"]:
|
|
151
|
+
return (
|
|
152
|
+
f"Successfully installed skill: {result['name']}\n"
|
|
153
|
+
f"Description: {result.get('description', '(none)')}\n"
|
|
154
|
+
f"Path: {result['path']}\n\n"
|
|
155
|
+
f"Use load_skill to activate it."
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
return f"Failed to install skill: {result['error']}"
|
|
159
|
+
|
|
160
|
+
elif action == "list":
|
|
161
|
+
skills = list_skills(include_system=False)
|
|
162
|
+
if not skills:
|
|
163
|
+
return "No user skills installed. Use action='install' to add skills."
|
|
164
|
+
lines = [f"Installed User Skills ({len(skills)}):"]
|
|
165
|
+
for skill in skills:
|
|
166
|
+
lines.append(f" - {skill.name}: {skill.description}")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
elif action == "uninstall":
|
|
170
|
+
if not name:
|
|
171
|
+
return "Error: 'name' is required for uninstall action"
|
|
172
|
+
result = uninstall_skill(name)
|
|
173
|
+
if result["success"]:
|
|
174
|
+
return f"Successfully uninstalled skill: {name}"
|
|
175
|
+
else:
|
|
176
|
+
return f"Failed to uninstall skill: {result['error']}"
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
return f"Unknown action: {action}. Use 'install', 'list', or 'uninstall'."
|
|
180
|
+
|
|
181
|
+
|
|
109
182
|
@tool(parse_docstring=True)
|
|
110
183
|
def think_tool(reflection: str) -> str:
|
|
111
184
|
"""Tool for strategic reflection on research progress and decision-making.
|