EvoScientist 0.0.1.dev4__py3-none-any.whl → 0.1.0rc1__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 +26 -62
- EvoScientist/__init__.py +0 -19
- EvoScientist/backends.py +0 -26
- EvoScientist/cli.py +1111 -498
- EvoScientist/middleware.py +8 -61
- EvoScientist/stream/__init__.py +0 -25
- EvoScientist/stream/utils.py +16 -23
- EvoScientist/tools.py +2 -75
- evoscientist-0.1.0rc1.dist-info/METADATA +199 -0
- evoscientist-0.1.0rc1.dist-info/RECORD +21 -0
- evoscientist-0.1.0rc1.dist-info/entry_points.txt +2 -0
- EvoScientist/config.py +0 -274
- EvoScientist/llm/__init__.py +0 -21
- EvoScientist/llm/models.py +0 -99
- EvoScientist/memory.py +0 -715
- EvoScientist/onboard.py +0 -725
- EvoScientist/paths.py +0 -44
- EvoScientist/skills/accelerate/SKILL.md +0 -332
- EvoScientist/skills/accelerate/references/custom-plugins.md +0 -453
- EvoScientist/skills/accelerate/references/megatron-integration.md +0 -489
- EvoScientist/skills/accelerate/references/performance.md +0 -525
- EvoScientist/skills/bitsandbytes/SKILL.md +0 -411
- EvoScientist/skills/bitsandbytes/references/memory-optimization.md +0 -521
- EvoScientist/skills/bitsandbytes/references/qlora-training.md +0 -521
- EvoScientist/skills/bitsandbytes/references/quantization-formats.md +0 -447
- EvoScientist/skills/find-skills/SKILL.md +0 -133
- EvoScientist/skills/find-skills/scripts/install_skill.py +0 -211
- EvoScientist/skills/flash-attention/SKILL.md +0 -367
- EvoScientist/skills/flash-attention/references/benchmarks.md +0 -215
- EvoScientist/skills/flash-attention/references/transformers-integration.md +0 -293
- EvoScientist/skills/llama-cpp/SKILL.md +0 -258
- EvoScientist/skills/llama-cpp/references/optimization.md +0 -89
- EvoScientist/skills/llama-cpp/references/quantization.md +0 -213
- EvoScientist/skills/llama-cpp/references/server.md +0 -125
- EvoScientist/skills/lm-evaluation-harness/SKILL.md +0 -490
- EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +0 -490
- EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +0 -488
- EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +0 -602
- EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +0 -519
- EvoScientist/skills/ml-paper-writing/SKILL.md +0 -937
- EvoScientist/skills/ml-paper-writing/references/checklists.md +0 -361
- EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +0 -562
- EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +0 -367
- EvoScientist/skills/ml-paper-writing/references/sources.md +0 -159
- EvoScientist/skills/ml-paper-writing/references/writing-guide.md +0 -476
- EvoScientist/skills/ml-paper-writing/templates/README.md +0 -251
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +0 -534
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +0 -144
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +0 -952
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +0 -111
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +0 -1493
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +0 -315
- EvoScientist/skills/ml-paper-writing/templates/acl/README.md +0 -50
- EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +0 -312
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +0 -377
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +0 -101
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +0 -1940
- EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +0 -26
- EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +0 -70
- EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +0 -326
- EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +0 -3
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +0 -11
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +0 -1440
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +0 -218
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +0 -305
- EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +0 -485
- EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +0 -508
- EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +0 -1246
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +0 -485
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +0 -24
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +0 -1440
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +0 -246
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +0 -414
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +0 -508
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +0 -1246
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +0 -79
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +0 -201
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +0 -75
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +0 -662
- EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +0 -864
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +0 -1443
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +0 -767
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +0 -36
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +0 -53
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +0 -38
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +0 -382
- EvoScientist/skills/peft/SKILL.md +0 -431
- EvoScientist/skills/peft/references/advanced-usage.md +0 -514
- EvoScientist/skills/peft/references/troubleshooting.md +0 -480
- EvoScientist/skills/ray-data/SKILL.md +0 -326
- EvoScientist/skills/ray-data/references/integration.md +0 -82
- EvoScientist/skills/ray-data/references/transformations.md +0 -83
- EvoScientist/skills/skill-creator/LICENSE.txt +0 -202
- EvoScientist/skills/skill-creator/SKILL.md +0 -356
- EvoScientist/skills/skill-creator/references/output-patterns.md +0 -82
- EvoScientist/skills/skill-creator/references/workflows.md +0 -28
- EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -303
- EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -110
- EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -95
- EvoScientist/skills_manager.py +0 -391
- EvoScientist/stream/display.py +0 -604
- EvoScientist/stream/events.py +0 -415
- EvoScientist/stream/state.py +0 -343
- evoscientist-0.0.1.dev4.dist-info/METADATA +0 -367
- evoscientist-0.0.1.dev4.dist-info/RECORD +0 -117
- evoscientist-0.0.1.dev4.dist-info/entry_points.txt +0 -5
- {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/WHEEL +0 -0
- {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/licenses/LICENSE +0 -0
- {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/top_level.txt +0 -0
EvoScientist/stream/state.py
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
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"
|