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.
Files changed (113) hide show
  1. EvoScientist/EvoScientist.py +26 -62
  2. EvoScientist/__init__.py +0 -19
  3. EvoScientist/backends.py +0 -26
  4. EvoScientist/cli.py +1111 -498
  5. EvoScientist/middleware.py +8 -61
  6. EvoScientist/stream/__init__.py +0 -25
  7. EvoScientist/stream/utils.py +16 -23
  8. EvoScientist/tools.py +2 -75
  9. evoscientist-0.1.0rc1.dist-info/METADATA +199 -0
  10. evoscientist-0.1.0rc1.dist-info/RECORD +21 -0
  11. evoscientist-0.1.0rc1.dist-info/entry_points.txt +2 -0
  12. EvoScientist/config.py +0 -274
  13. EvoScientist/llm/__init__.py +0 -21
  14. EvoScientist/llm/models.py +0 -99
  15. EvoScientist/memory.py +0 -715
  16. EvoScientist/onboard.py +0 -725
  17. EvoScientist/paths.py +0 -44
  18. EvoScientist/skills/accelerate/SKILL.md +0 -332
  19. EvoScientist/skills/accelerate/references/custom-plugins.md +0 -453
  20. EvoScientist/skills/accelerate/references/megatron-integration.md +0 -489
  21. EvoScientist/skills/accelerate/references/performance.md +0 -525
  22. EvoScientist/skills/bitsandbytes/SKILL.md +0 -411
  23. EvoScientist/skills/bitsandbytes/references/memory-optimization.md +0 -521
  24. EvoScientist/skills/bitsandbytes/references/qlora-training.md +0 -521
  25. EvoScientist/skills/bitsandbytes/references/quantization-formats.md +0 -447
  26. EvoScientist/skills/find-skills/SKILL.md +0 -133
  27. EvoScientist/skills/find-skills/scripts/install_skill.py +0 -211
  28. EvoScientist/skills/flash-attention/SKILL.md +0 -367
  29. EvoScientist/skills/flash-attention/references/benchmarks.md +0 -215
  30. EvoScientist/skills/flash-attention/references/transformers-integration.md +0 -293
  31. EvoScientist/skills/llama-cpp/SKILL.md +0 -258
  32. EvoScientist/skills/llama-cpp/references/optimization.md +0 -89
  33. EvoScientist/skills/llama-cpp/references/quantization.md +0 -213
  34. EvoScientist/skills/llama-cpp/references/server.md +0 -125
  35. EvoScientist/skills/lm-evaluation-harness/SKILL.md +0 -490
  36. EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +0 -490
  37. EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +0 -488
  38. EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +0 -602
  39. EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +0 -519
  40. EvoScientist/skills/ml-paper-writing/SKILL.md +0 -937
  41. EvoScientist/skills/ml-paper-writing/references/checklists.md +0 -361
  42. EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +0 -562
  43. EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +0 -367
  44. EvoScientist/skills/ml-paper-writing/references/sources.md +0 -159
  45. EvoScientist/skills/ml-paper-writing/references/writing-guide.md +0 -476
  46. EvoScientist/skills/ml-paper-writing/templates/README.md +0 -251
  47. EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +0 -534
  48. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +0 -144
  49. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +0 -952
  50. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +0 -111
  51. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +0 -1493
  52. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +0 -315
  53. EvoScientist/skills/ml-paper-writing/templates/acl/README.md +0 -50
  54. EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +0 -312
  55. EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +0 -377
  56. EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +0 -101
  57. EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +0 -1940
  58. EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +0 -26
  59. EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +0 -70
  60. EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +0 -326
  61. EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +0 -3
  62. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +0 -11
  63. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +0 -1440
  64. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  65. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +0 -218
  66. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +0 -305
  67. EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +0 -485
  68. EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +0 -508
  69. EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +0 -1246
  70. EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +0 -485
  71. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +0 -24
  72. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +0 -1440
  73. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  74. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +0 -246
  75. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +0 -414
  76. EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +0 -508
  77. EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +0 -1246
  78. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +0 -79
  79. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +0 -201
  80. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +0 -75
  81. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  82. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +0 -662
  83. EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +0 -864
  84. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +0 -1443
  85. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +0 -767
  86. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  87. EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +0 -36
  88. EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +0 -53
  89. EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +0 -38
  90. EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +0 -382
  91. EvoScientist/skills/peft/SKILL.md +0 -431
  92. EvoScientist/skills/peft/references/advanced-usage.md +0 -514
  93. EvoScientist/skills/peft/references/troubleshooting.md +0 -480
  94. EvoScientist/skills/ray-data/SKILL.md +0 -326
  95. EvoScientist/skills/ray-data/references/integration.md +0 -82
  96. EvoScientist/skills/ray-data/references/transformations.md +0 -83
  97. EvoScientist/skills/skill-creator/LICENSE.txt +0 -202
  98. EvoScientist/skills/skill-creator/SKILL.md +0 -356
  99. EvoScientist/skills/skill-creator/references/output-patterns.md +0 -82
  100. EvoScientist/skills/skill-creator/references/workflows.md +0 -28
  101. EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -303
  102. EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -110
  103. EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -95
  104. EvoScientist/skills_manager.py +0 -391
  105. EvoScientist/stream/display.py +0 -604
  106. EvoScientist/stream/events.py +0 -415
  107. EvoScientist/stream/state.py +0 -343
  108. evoscientist-0.0.1.dev4.dist-info/METADATA +0 -367
  109. evoscientist-0.0.1.dev4.dist-info/RECORD +0 -117
  110. evoscientist-0.0.1.dev4.dist-info/entry_points.txt +0 -5
  111. {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/WHEEL +0 -0
  112. {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/licenses/LICENSE +0 -0
  113. {evoscientist-0.0.1.dev4.dist-info → evoscientist-0.1.0rc1.dist-info}/top_level.txt +0 -0
EvoScientist/memory.py DELETED
@@ -1,715 +0,0 @@
1
- """EvoScientist Memory Middleware.
2
-
3
- Automatically extracts and persists long-term memory (user profile, research
4
- preferences, experiment conclusions) from conversations.
5
-
6
- Two mechanisms:
7
- 1. **Injection** (every LLM call): Reads ``/memory/MEMORY.md`` and appends it
8
- to the system prompt so the agent always has context.
9
- 2. **Extraction** (threshold-triggered): When the conversation exceeds a
10
- configurable message count, uses an LLM call to pull out structured facts
11
- and merges them into the appropriate MEMORY.md sections.
12
-
13
- ## Usage
14
-
15
- ```python
16
- from EvoScientist.memory import EvoMemoryMiddleware
17
-
18
- middleware = EvoMemoryMiddleware(
19
- backend=my_backend, # or backend factory
20
- memory_path="/memory/MEMORY.md",
21
- extraction_model=chat_model,
22
- trigger=("messages", 20),
23
- )
24
- agent = create_deep_agent(middleware=[middleware, ...])
25
- ```
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import logging
31
- import re
32
- from collections.abc import Awaitable, Callable
33
- from contextvars import ContextVar
34
- from typing import TYPE_CHECKING, Any, Annotated, NotRequired, cast
35
-
36
- from langchain.agents.middleware.types import (
37
- AgentMiddleware,
38
- AgentState,
39
- ModelRequest,
40
- ModelResponse,
41
- PrivateStateAttr,
42
- )
43
- from langchain.tools import ToolRuntime
44
- from langchain_core.messages import AnyMessage, HumanMessage, AIMessage
45
- from langchain_core.runnables.config import RunnableConfig
46
- from langgraph.runtime import Runtime
47
-
48
- if TYPE_CHECKING:
49
- from langchain.chat_models import BaseChatModel
50
- from deepagents.backends.protocol import BACKEND_TYPES, BackendProtocol
51
-
52
- logger = logging.getLogger(__name__)
53
-
54
- _CURRENT_MEMORY: ContextVar[str] = ContextVar("evo_memory_current", default="")
55
- _STATE_MEMORY_KEY = "evo_memory_content"
56
-
57
-
58
- class EvoMemoryState(AgentState):
59
- """State schema for EvoMemoryMiddleware."""
60
-
61
- evo_memory_content: NotRequired[Annotated[str, PrivateStateAttr]]
62
-
63
- # ---------------------------------------------------------------------------
64
- # Extraction prompt – sent to a (cheap) LLM to pull structured facts
65
- # ---------------------------------------------------------------------------
66
-
67
- EXTRACTION_PROMPT = """\
68
- You are a memory extraction assistant for a scientific experiment agent called EvoScientist.
69
-
70
- Analyze the following conversation and extract any NEW information that should be
71
- remembered long-term. Only extract facts that are **not already present** in the
72
- current memory shown below.
73
-
74
- <current_memory>
75
- {current_memory}
76
- </current_memory>
77
-
78
- <conversation>
79
- {conversation}
80
- </conversation>
81
-
82
- Return a JSON object with ONLY the keys that have new information to add.
83
- Omit keys where there is nothing new. Use `null` for unknown values.
84
-
85
- ```json
86
- {{
87
- "user_profile": {{
88
- "name": "string or null",
89
- "role": "string or null",
90
- "institution": "string or null",
91
- "language": "string or null"
92
- }},
93
- "research_preferences": {{
94
- "primary_domain": "string or null",
95
- "sub_fields": "string or null",
96
- "preferred_frameworks": "string or null",
97
- "preferred_models": "string or null",
98
- "hardware": "string or null",
99
- "constraints": "string or null"
100
- }},
101
- "experiment_conclusion": {{
102
- "title": "string – experiment name",
103
- "question": "string – research question",
104
- "method": "string – method summary",
105
- "key_result": "string – primary metric/outcome",
106
- "conclusion": "string – one-line conclusion",
107
- "artifacts": "string – report path if any"
108
- }},
109
- "learned_preferences": [
110
- "string – each new preference or habit observed"
111
- ]
112
- }}
113
- ```
114
-
115
- Rules:
116
- - Only return keys with genuinely new information.
117
- - If nothing new was found, return an empty JSON object: `{{}}`
118
- - Do NOT repeat information already in <current_memory>.
119
- - For experiment_conclusion, only include if a complete experiment was actually run.
120
- - Be concise. Each value should be a short phrase, not a paragraph.
121
- """
122
-
123
- # ---------------------------------------------------------------------------
124
- # System-prompt snippet injected every turn
125
- # ---------------------------------------------------------------------------
126
-
127
- MEMORY_INJECTION_TEMPLATE = """<evo_memory>
128
- {memory_content}
129
- </evo_memory>
130
-
131
- <memory_instructions>
132
- The above <evo_memory> contains your long-term memory about the user and past experiments.
133
- Use this to personalize your responses and avoid re-asking known information.
134
-
135
- **When to update memory:**
136
- - User shares their name, role, institution, or language
137
- - User mentions their research domain, preferred frameworks, models, or hardware
138
- - User explicitly asks you to remember something
139
- - An experiment completes with notable conclusions
140
-
141
- **How to update memory:**
142
- - If /memory/MEMORY.md does not exist yet, use `write_file` to create it
143
- - If it already exists, use `edit_file` to update specific sections
144
- - Use this markdown structure:
145
-
146
- ```markdown
147
- # EvoScientist Memory
148
-
149
- ## User Profile
150
- - **Name**: ...
151
- - **Role**: ...
152
- - **Institution**: ...
153
- - **Language**: ...
154
-
155
- ## Research Preferences
156
- - **Primary Domain**: ...
157
- - **Sub-fields**: ...
158
- - **Preferred Frameworks**: ...
159
- - **Preferred Models**: ...
160
- - **Hardware**: ...
161
- - **Constraints**: ...
162
-
163
- ## Experiment History
164
- ### [YYYY-MM-DD] Experiment Title
165
- - **Question**: ...
166
- - **Key Result**: ...
167
- - **Conclusion**: ...
168
-
169
- ## Learned Preferences
170
- - ...
171
- ```
172
-
173
- **Priority:** Update memory IMMEDIATELY when the user provides personal or research
174
- information — before composing your main response.
175
- </memory_instructions>"""
176
-
177
- DEFAULT_MEMORY_TEMPLATE = """# EvoScientist Memory
178
-
179
- ## User Profile
180
- - **Name**: (unknown)
181
- - **Role**: (unknown)
182
- - **Institution**: (unknown)
183
- - **Language**: (unknown)
184
-
185
- ## Research Preferences
186
- - **Primary Domain**: (unknown)
187
- - **Sub-fields**: (unknown)
188
- - **Preferred Frameworks**: (unknown)
189
- - **Preferred Models**: (unknown)
190
- - **Hardware**: (unknown)
191
- - **Constraints**: (unknown)
192
-
193
- ## Experiment History
194
- (No experiments yet)
195
-
196
- ## Learned Preferences
197
- - (none yet)
198
- """
199
-
200
-
201
- def _get_thread_id(runtime: Runtime) -> str:
202
- try:
203
- config = cast("RunnableConfig", getattr(runtime, "config", {}))
204
- if isinstance(config, dict):
205
- thread_id = config.get("configurable", {}).get("thread_id")
206
- if thread_id is not None:
207
- return str(thread_id)
208
- except Exception: # noqa: BLE001
209
- logger.debug("Failed to resolve thread_id from runtime config")
210
- return "default"
211
-
212
-
213
- def _ensure_section(content: str, marker: str, body: str) -> str:
214
- if marker in content:
215
- return content
216
- content = content.rstrip()
217
- if content:
218
- content += "\n\n"
219
- return f"{content}{marker}\n{body.rstrip()}\n"
220
-
221
-
222
- def _ensure_memory_template(existing_md: str) -> str:
223
- if not existing_md.strip():
224
- return DEFAULT_MEMORY_TEMPLATE
225
-
226
- result = existing_md
227
- if "# EvoScientist Memory" not in result:
228
- result = "# EvoScientist Memory\n\n" + result.lstrip()
229
-
230
- result = _ensure_section(
231
- result,
232
- "## User Profile",
233
- "\n".join(
234
- [
235
- "- **Name**: (unknown)",
236
- "- **Role**: (unknown)",
237
- "- **Institution**: (unknown)",
238
- "- **Language**: (unknown)",
239
- ],
240
- ),
241
- )
242
- result = _ensure_section(
243
- result,
244
- "## Research Preferences",
245
- "\n".join(
246
- [
247
- "- **Primary Domain**: (unknown)",
248
- "- **Sub-fields**: (unknown)",
249
- "- **Preferred Frameworks**: (unknown)",
250
- "- **Preferred Models**: (unknown)",
251
- "- **Hardware**: (unknown)",
252
- "- **Constraints**: (unknown)",
253
- ],
254
- ),
255
- )
256
- result = _ensure_section(result, "## Experiment History", "(No experiments yet)")
257
- result = _ensure_section(result, "## Learned Preferences", "- (none yet)")
258
- return result
259
-
260
-
261
- def _section_bounds(content: str, marker: str) -> tuple[int | None, int | None]:
262
- idx = content.find(marker)
263
- if idx == -1:
264
- return None, None
265
- start = idx + len(marker)
266
- next_marker = content.find("\n## ", start)
267
- if next_marker == -1:
268
- next_marker = len(content)
269
- return start, next_marker
270
-
271
-
272
- def _normalize_item(value: str) -> str:
273
- return re.sub(r"\s+", " ", value.strip().lower())
274
-
275
-
276
- # ---------------------------------------------------------------------------
277
- # Helper: merge extracted JSON into MEMORY.md markdown
278
- # ---------------------------------------------------------------------------
279
-
280
- def _merge_memory(existing_md: str, extracted: dict[str, Any]) -> str:
281
- """Merge extracted fields into the existing MEMORY.md content.
282
-
283
- Performs targeted replacements within the known sections. Unknown
284
- sections or empty extractions are left untouched.
285
- """
286
- if not extracted:
287
- return existing_md
288
-
289
- result = _ensure_memory_template(existing_md)
290
-
291
- # --- User Profile ---
292
- profile = extracted.get("user_profile")
293
- if profile and isinstance(profile, dict):
294
- field_map = {
295
- "name": "Name",
296
- "role": "Role",
297
- "institution": "Institution",
298
- "language": "Language",
299
- }
300
- for key, label in field_map.items():
301
- value = profile.get(key)
302
- if value and value != "null":
303
- # Replace the line "- **Label**: ..." with new value
304
- pattern = rf"(- \*\*{label}\*\*: ).*"
305
- replacement = rf"\g<1>{value}"
306
- result = re.sub(pattern, replacement, result)
307
-
308
- # --- Research Preferences ---
309
- prefs = extracted.get("research_preferences")
310
- if prefs and isinstance(prefs, dict):
311
- field_map = {
312
- "primary_domain": "Primary Domain",
313
- "sub_fields": "Sub-fields",
314
- "preferred_frameworks": "Preferred Frameworks",
315
- "preferred_models": "Preferred Models",
316
- "hardware": "Hardware",
317
- "constraints": "Constraints",
318
- }
319
- for key, label in field_map.items():
320
- value = prefs.get(key)
321
- if value and value != "null":
322
- pattern = rf"(- \*\*{label}\*\*: ).*"
323
- replacement = rf"\g<1>{value}"
324
- result = re.sub(pattern, replacement, result)
325
-
326
- # --- Experiment History (append) ---
327
- exp = extracted.get("experiment_conclusion")
328
- should_add_exp = bool(exp and isinstance(exp, dict) and exp.get("title"))
329
- if should_add_exp:
330
- from datetime import datetime
331
- date_str = datetime.now().strftime("%Y-%m-%d")
332
- title = str(exp.get("title", "Untitled")).strip()
333
- entry = f"\n### [{date_str}] {title}\n"
334
- entry += f"- **Question**: {exp.get('question', 'N/A')}\n"
335
- entry += f"- **Method**: {exp.get('method', 'N/A')}\n"
336
- entry += f"- **Key Result**: {exp.get('key_result', 'N/A')}\n"
337
- entry += f"- **Conclusion**: {exp.get('conclusion', 'N/A')}\n"
338
- if exp.get("artifacts"):
339
- entry += f"- **Artifacts**: {exp['artifacts']}\n"
340
-
341
- # Remove placeholder if present
342
- exp_start, exp_end = _section_bounds(result, "## Experiment History")
343
- if exp_start is not None and exp_end is not None:
344
- exp_section = result[exp_start:exp_end]
345
- exp_lines = [
346
- line for line in exp_section.splitlines() if "(No experiments yet)" not in line
347
- ]
348
- result = result[:exp_start] + "\n" + "\n".join(exp_lines).strip("\n") + "\n" + result[exp_end:]
349
-
350
- # De-duplicate by title if already present
351
- if re.search(rf"### \[[0-9-]+\] {re.escape(title)}\b", result):
352
- should_add_exp = False
353
-
354
- if should_add_exp and exp and isinstance(exp, dict) and exp.get("title"):
355
- # Insert before "## Learned Preferences"
356
- marker = "## Learned Preferences"
357
- if marker in result:
358
- result = result.replace(marker, entry + "\n" + marker)
359
- else:
360
- # Fallback: append at end
361
- result = result.rstrip() + "\n" + entry
362
-
363
- # --- Learned Preferences (append) ---
364
- learned = extracted.get("learned_preferences")
365
- if learned and isinstance(learned, list):
366
- marker = "## Learned Preferences"
367
- start, end = _section_bounds(result, marker)
368
- if start is None or end is None:
369
- result = _ensure_section(result, marker, "- (none yet)")
370
- start, end = _section_bounds(result, marker)
371
-
372
- if start is not None and end is not None:
373
- section = result[start:end]
374
- section_lines = [
375
- line for line in section.splitlines()
376
- if line.strip() and line.strip() not in {"- (none yet)", "- (none)"}
377
- ]
378
- existing_items = {
379
- _normalize_item(line[2:])
380
- for line in section_lines
381
- if line.strip().startswith("- ")
382
- }
383
- new_lines = []
384
- for item in learned:
385
- if not item:
386
- continue
387
- normalized = _normalize_item(str(item))
388
- if normalized in existing_items:
389
- continue
390
- existing_items.add(normalized)
391
- new_lines.append(f"- {item}")
392
-
393
- if new_lines:
394
- section_lines.extend(new_lines)
395
- rebuilt = "\n" + "\n".join(section_lines).strip("\n") + "\n"
396
- result = result[:start] + rebuilt + result[end:]
397
-
398
- return result
399
-
400
-
401
- # ---------------------------------------------------------------------------
402
- # Middleware
403
- # ---------------------------------------------------------------------------
404
-
405
- class EvoMemoryMiddleware(AgentMiddleware):
406
- """Middleware that injects and auto-extracts long-term memory.
407
-
408
- Args:
409
- backend: Backend instance or factory for reading/writing memory files.
410
- memory_path: Virtual path to MEMORY.md (default ``/memory/MEMORY.md``).
411
- extraction_model: Chat model used for extraction (can be a cheap/fast
412
- model like ``claude-haiku``). If ``None``, automatic extraction is
413
- disabled and only prompt injection + manual ``edit_file`` works.
414
- trigger: When to run automatic extraction. Supports
415
- ``("messages", N)`` to trigger every *N* human messages.
416
- Defaults to ``("messages", 20)``.
417
- """
418
-
419
- state_schema = EvoMemoryState
420
-
421
- def __init__(
422
- self,
423
- *,
424
- backend: BACKEND_TYPES,
425
- memory_path: str = "/memory/MEMORY.md",
426
- extraction_model: BaseChatModel | None = None,
427
- trigger: tuple[str, int] = ("messages", 20),
428
- ) -> None:
429
- self._backend = backend
430
- self._memory_path = memory_path
431
- self._extraction_model = extraction_model
432
- self._trigger = trigger
433
- self._last_extraction_at: dict[str, int] = {} # message count per thread
434
-
435
- # -- backend resolution --------------------------------------------------
436
-
437
- def _get_backend(
438
- self,
439
- state: AgentState[Any],
440
- runtime: Runtime,
441
- ) -> BackendProtocol:
442
- if callable(self._backend):
443
- config = cast("RunnableConfig", getattr(runtime, "config", {}))
444
- tool_runtime = ToolRuntime(
445
- state=state,
446
- context=runtime.context,
447
- stream_writer=runtime.stream_writer,
448
- store=runtime.store,
449
- config=config,
450
- tool_call_id=None,
451
- )
452
- return self._backend(tool_runtime)
453
- return self._backend
454
-
455
- # -- agent-level preload -------------------------------------------------
456
-
457
- def before_agent(
458
- self,
459
- state: AgentState[Any],
460
- runtime: Runtime,
461
- config: RunnableConfig,
462
- ) -> dict[str, Any] | None:
463
- if state.get(_STATE_MEMORY_KEY) is not None:
464
- return None
465
- backend = self._get_backend(state, runtime)
466
- memory = self._read_memory(backend)
467
- _CURRENT_MEMORY.set(memory)
468
- return {_STATE_MEMORY_KEY: memory}
469
-
470
- async def abefore_agent(
471
- self,
472
- state: AgentState[Any],
473
- runtime: Runtime,
474
- config: RunnableConfig,
475
- ) -> dict[str, Any] | None:
476
- if state.get(_STATE_MEMORY_KEY) is not None:
477
- return None
478
- backend = self._get_backend(state, runtime)
479
- memory = await self._aread_memory(backend)
480
- _CURRENT_MEMORY.set(memory)
481
- return {_STATE_MEMORY_KEY: memory}
482
-
483
- # -- read / write helpers ------------------------------------------------
484
-
485
- def _read_memory(self, backend: BackendProtocol) -> str:
486
- """Read MEMORY.md content (raw bytes → str)."""
487
- try:
488
- responses = backend.download_files([self._memory_path])
489
- if responses and responses[0].content is not None and responses[0].error is None:
490
- return responses[0].content.decode("utf-8")
491
- except Exception as e: # noqa: BLE001
492
- logger.debug("Failed to read memory at %s: %s", self._memory_path, e)
493
- return ""
494
-
495
- async def _aread_memory(self, backend: BackendProtocol) -> str:
496
- try:
497
- responses = await backend.adownload_files([self._memory_path])
498
- if responses and responses[0].content is not None and responses[0].error is None:
499
- return responses[0].content.decode("utf-8")
500
- except Exception as e: # noqa: BLE001
501
- logger.debug("Failed to read memory at %s: %s", self._memory_path, e)
502
- return ""
503
-
504
- def _write_memory(self, backend: BackendProtocol, old_content: str, new_content: str) -> None:
505
- """Write updated MEMORY.md (edit if exists, write if new)."""
506
- try:
507
- if old_content:
508
- result = backend.edit(self._memory_path, old_content, new_content)
509
- else:
510
- result = backend.write(self._memory_path, new_content)
511
- if result and result.error:
512
- logger.warning("Failed to write memory: %s", result.error)
513
- except Exception as e: # noqa: BLE001
514
- logger.warning("Exception writing memory: %s", e)
515
-
516
- async def _awrite_memory(self, backend: BackendProtocol, old_content: str, new_content: str) -> None:
517
- try:
518
- if old_content:
519
- result = await backend.aedit(self._memory_path, old_content, new_content)
520
- else:
521
- result = await backend.awrite(self._memory_path, new_content)
522
- if result and result.error:
523
- logger.warning("Failed to write memory: %s", result.error)
524
- except Exception as e: # noqa: BLE001
525
- logger.warning("Exception writing memory: %s", e)
526
-
527
- # -- threshold check -----------------------------------------------------
528
-
529
- def _should_extract(self, thread_id: str, messages: list[AnyMessage]) -> bool:
530
- """Check if we should run automatic extraction."""
531
- if self._extraction_model is None:
532
- return False
533
-
534
- trigger_type, trigger_value = self._trigger
535
- if trigger_type == "messages":
536
- human_count = sum(1 for m in messages if isinstance(m, HumanMessage))
537
- last = self._last_extraction_at.get(thread_id, 0)
538
- return (human_count - last) >= trigger_value
539
- return False
540
-
541
- # -- extraction ----------------------------------------------------------
542
-
543
- def _extract(self, model: BaseChatModel, memory: str, messages: list[AnyMessage]) -> dict[str, Any]:
544
- """Run LLM extraction on recent messages."""
545
- import json
546
-
547
- # Build conversation string from recent messages (last 30)
548
- recent = messages[-30:]
549
- conv_parts = []
550
- for msg in recent:
551
- if isinstance(msg, HumanMessage):
552
- role = "user"
553
- elif isinstance(msg, AIMessage):
554
- role = "assistant"
555
- else:
556
- continue
557
- content = msg.content if isinstance(msg.content, str) else str(msg.content)
558
- conv_parts.append(f"[{role}]: {content}")
559
- conversation = "\n".join(conv_parts)
560
-
561
- prompt = EXTRACTION_PROMPT.format(
562
- current_memory=memory,
563
- conversation=conversation,
564
- )
565
-
566
- try:
567
- response = model.invoke(prompt)
568
- text = response.content if isinstance(response.content, str) else str(response.content)
569
- # Extract JSON from response (may be wrapped in ```json ... ```)
570
- json_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
571
- if json_match:
572
- text = json_match.group(1)
573
- return json.loads(text.strip())
574
- except Exception as e: # noqa: BLE001
575
- logger.warning("Memory extraction failed: %s", e)
576
- return {}
577
-
578
- async def _aextract(self, model: BaseChatModel, memory: str, messages: list[AnyMessage]) -> dict[str, Any]:
579
- import json
580
-
581
- recent = messages[-30:]
582
- conv_parts = []
583
- for msg in recent:
584
- if isinstance(msg, HumanMessage):
585
- role = "user"
586
- elif isinstance(msg, AIMessage):
587
- role = "assistant"
588
- else:
589
- continue
590
- content = msg.content if isinstance(msg.content, str) else str(msg.content)
591
- conv_parts.append(f"[{role}]: {content}")
592
- conversation = "\n".join(conv_parts)
593
-
594
- prompt = EXTRACTION_PROMPT.format(
595
- current_memory=memory,
596
- conversation=conversation,
597
- )
598
-
599
- try:
600
- response = await model.ainvoke(prompt)
601
- text = response.content if isinstance(response.content, str) else str(response.content)
602
- json_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
603
- if json_match:
604
- text = json_match.group(1)
605
- return json.loads(text.strip())
606
- except Exception as e: # noqa: BLE001
607
- logger.warning("Memory extraction failed: %s", e)
608
- return {}
609
-
610
- # -- middleware hooks -----------------------------------------------------
611
-
612
- def modify_request(self, request: ModelRequest) -> ModelRequest:
613
- """Inject memory content and instructions into the system message.
614
-
615
- Always injects ``<memory_instructions>`` so the agent knows it can
616
- save memories, even when MEMORY.md does not exist yet.
617
- """
618
- state = request.state or {}
619
- memory_content = state.get(_STATE_MEMORY_KEY, "")
620
- if not memory_content:
621
- memory_content = _CURRENT_MEMORY.get()
622
- if not memory_content and request.runtime is not None:
623
- try:
624
- backend = self._get_backend(state, request.runtime)
625
- memory_content = self._read_memory(backend)
626
- _CURRENT_MEMORY.set(memory_content)
627
- except Exception as e: # noqa: BLE001
628
- logger.debug("Failed to load memory during modify_request: %s", e)
629
- # Use placeholder when memory file doesn't exist yet
630
- if not memory_content:
631
- memory_content = "(No memory saved yet. Create /memory/MEMORY.md when you learn important information.)"
632
-
633
- from deepagents.middleware._utils import append_to_system_message
634
- injection = MEMORY_INJECTION_TEMPLATE.format(memory_content=memory_content)
635
- new_system = append_to_system_message(request.system_message, injection)
636
- return request.override(system_message=new_system)
637
-
638
- def wrap_model_call(
639
- self,
640
- request: ModelRequest,
641
- handler: Callable[[ModelRequest], ModelResponse],
642
- ) -> ModelResponse:
643
- """Inject memory into system prompt before every LLM call."""
644
- modified = self.modify_request(request)
645
- return handler(modified)
646
-
647
- async def awrap_model_call(
648
- self,
649
- request: ModelRequest,
650
- handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
651
- ) -> ModelResponse:
652
- modified = self.modify_request(request)
653
- return await handler(modified)
654
-
655
- def before_model(
656
- self,
657
- state: AgentState[Any],
658
- runtime: Runtime,
659
- ) -> dict[str, Any] | None:
660
- """Read memory and optionally run extraction before each LLM call."""
661
- backend = self._get_backend(state, runtime)
662
- messages = state["messages"]
663
- thread_id = _get_thread_id(runtime)
664
-
665
- # Always read memory for injection
666
- memory = self._read_memory(backend)
667
- _CURRENT_MEMORY.set(memory)
668
- state_update: dict[str, Any] | None = None
669
- if state.get(_STATE_MEMORY_KEY) != memory:
670
- state_update = {_STATE_MEMORY_KEY: memory}
671
-
672
- # Check extraction threshold
673
- if self._should_extract(thread_id, messages):
674
- human_count = sum(1 for m in messages if isinstance(m, HumanMessage))
675
- extracted = self._extract(self._extraction_model, memory, messages)
676
- if extracted:
677
- new_memory = _merge_memory(memory, extracted)
678
- if new_memory != memory:
679
- self._write_memory(backend, memory, new_memory)
680
- _CURRENT_MEMORY.set(new_memory)
681
- logger.info("Auto-extracted and updated memory")
682
- state_update = {_STATE_MEMORY_KEY: new_memory}
683
- self._last_extraction_at[thread_id] = human_count
684
-
685
- return state_update
686
-
687
- async def abefore_model(
688
- self,
689
- state: AgentState[Any],
690
- runtime: Runtime,
691
- ) -> dict[str, Any] | None:
692
- """Async: Read memory and optionally run extraction."""
693
- backend = self._get_backend(state, runtime)
694
- messages = state["messages"]
695
- thread_id = _get_thread_id(runtime)
696
-
697
- memory = await self._aread_memory(backend)
698
- _CURRENT_MEMORY.set(memory)
699
- state_update: dict[str, Any] | None = None
700
- if state.get(_STATE_MEMORY_KEY) != memory:
701
- state_update = {_STATE_MEMORY_KEY: memory}
702
-
703
- if self._should_extract(thread_id, messages):
704
- human_count = sum(1 for m in messages if isinstance(m, HumanMessage))
705
- extracted = await self._aextract(self._extraction_model, memory, messages)
706
- if extracted:
707
- new_memory = _merge_memory(memory, extracted)
708
- if new_memory != memory:
709
- await self._awrite_memory(backend, memory, new_memory)
710
- _CURRENT_MEMORY.set(new_memory)
711
- logger.info("Auto-extracted and updated memory")
712
- state_update = {_STATE_MEMORY_KEY: new_memory}
713
- self._last_extraction_at[thread_id] = human_count
714
-
715
- return state_update