ocerebro 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/ocerebro.js +98 -0
- package/cerebro/__init__.py +7 -0
- package/cerebro/__main__.py +19 -0
- package/cerebro/cerebro_setup.py +459 -0
- package/hooks/__init__.py +1 -0
- package/hooks/cost_hook.py +45 -0
- package/hooks/coverage_hook.py +39 -0
- package/hooks/error_hook.py +48 -0
- package/hooks/expensive_hook.py +41 -0
- package/hooks/global_logger.py +32 -0
- package/package.json +49 -0
- package/pyproject.toml +77 -0
- package/src/__init__.py +2 -0
- package/src/cli/__init__.py +2 -0
- package/src/cli/dream.py +91 -0
- package/src/cli/gc.py +93 -0
- package/src/cli/main.py +583 -0
- package/src/cli/remember.py +74 -0
- package/src/consolidation/__init__.py +8 -0
- package/src/consolidation/checkpoints.py +96 -0
- package/src/consolidation/dream.py +465 -0
- package/src/consolidation/extractor.py +313 -0
- package/src/consolidation/promoter.py +435 -0
- package/src/consolidation/remember.py +544 -0
- package/src/consolidation/scorer.py +191 -0
- package/src/core/__init__.py +6 -0
- package/src/core/event_schema.py +55 -0
- package/src/core/jsonl_storage.py +238 -0
- package/src/core/paths.py +254 -0
- package/src/core/session_manager.py +76 -0
- package/src/diff/__init__.py +5 -0
- package/src/diff/memory_diff.py +571 -0
- package/src/forgetting/__init__.py +6 -0
- package/src/forgetting/decay.py +86 -0
- package/src/forgetting/gc.py +296 -0
- package/src/forgetting/guard_rails.py +126 -0
- package/src/hooks/__init__.py +11 -0
- package/src/hooks/core_captures.py +170 -0
- package/src/hooks/custom_loader.py +389 -0
- package/src/index/__init__.py +7 -0
- package/src/index/embeddings_db.py +419 -0
- package/src/index/metadata_db.py +230 -0
- package/src/index/queries.py +357 -0
- package/src/mcp/__init__.py +2 -0
- package/src/mcp/server.py +640 -0
- package/src/memdir/__init__.py +19 -0
- package/src/memdir/scanner.py +260 -0
- package/src/official/__init__.py +5 -0
- package/src/official/markdown_storage.py +173 -0
- package/src/official/templates.py +128 -0
- package/src/working/__init__.py +5 -0
- package/src/working/memory_view.py +150 -0
- package/src/working/yaml_storage.py +234 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Gerenciamento de triggers de checkpoint"""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CheckpointTrigger(Enum):
|
|
8
|
+
"""Tipos de triggers de checkpoint"""
|
|
9
|
+
FEATURE_DONE = "feature_done"
|
|
10
|
+
SESSION_END = "session_end"
|
|
11
|
+
ERROR_CRITICAL = "error_critical"
|
|
12
|
+
MANUAL = "manual"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CheckpointManager:
|
|
16
|
+
"""
|
|
17
|
+
Gerencia triggers de checkpoint.
|
|
18
|
+
|
|
19
|
+
Triggers disponíveis:
|
|
20
|
+
- feature_done: Feature implementada com testes passando
|
|
21
|
+
- session_end: Fim de sessão do desenvolvedor
|
|
22
|
+
- error_critical: Erro crítico que deve ser documentado
|
|
23
|
+
- manual: Trigger manual solicitada pelo usuário
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config_path):
|
|
27
|
+
"""
|
|
28
|
+
Inicializa o CheckpointManager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_path: Path para configuração
|
|
32
|
+
"""
|
|
33
|
+
self.config_path = config_path
|
|
34
|
+
|
|
35
|
+
def check_triggers(self, context: Dict) -> List[CheckpointTrigger]:
|
|
36
|
+
"""
|
|
37
|
+
Verifica triggers baseado no contexto.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
context: Dicionário com dados do contexto
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Lista de triggers ativados
|
|
44
|
+
"""
|
|
45
|
+
triggers = []
|
|
46
|
+
|
|
47
|
+
if context.get("tests_passed") and context.get("files_changed"):
|
|
48
|
+
triggers.append(CheckpointTrigger.FEATURE_DONE)
|
|
49
|
+
|
|
50
|
+
if context.get("session_ending"):
|
|
51
|
+
triggers.append(CheckpointTrigger.SESSION_END)
|
|
52
|
+
|
|
53
|
+
if context.get("error_severity") in ["critical", "high"]:
|
|
54
|
+
triggers.append(CheckpointTrigger.ERROR_CRITICAL)
|
|
55
|
+
|
|
56
|
+
return triggers
|
|
57
|
+
|
|
58
|
+
def should_checkpoint(self, context: Dict) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Decide se deve fazer checkpoint.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
context: Dicionário com dados do contexto
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True se deve fazer checkpoint, False caso contrário
|
|
67
|
+
"""
|
|
68
|
+
return len(self.check_triggers(context)) > 0
|
|
69
|
+
|
|
70
|
+
def get_trigger_reason(self, context: Dict) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Obtém descrição do motivo do checkpoint.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
context: Dicionário com dados do contexto
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
String descrevendo o motivo
|
|
79
|
+
"""
|
|
80
|
+
triggers = self.check_triggers(context)
|
|
81
|
+
|
|
82
|
+
if not triggers:
|
|
83
|
+
return "Nenhum trigger ativado"
|
|
84
|
+
|
|
85
|
+
reasons = []
|
|
86
|
+
for trigger in triggers:
|
|
87
|
+
if trigger == CheckpointTrigger.FEATURE_DONE:
|
|
88
|
+
reasons.append("Feature concluída com testes passando")
|
|
89
|
+
elif trigger == CheckpointTrigger.SESSION_END:
|
|
90
|
+
reasons.append("Fim de sessão")
|
|
91
|
+
elif trigger == CheckpointTrigger.ERROR_CRITICAL:
|
|
92
|
+
reasons.append("Erro crítico detectado")
|
|
93
|
+
elif trigger == CheckpointTrigger.MANUAL:
|
|
94
|
+
reasons.append("Solicitação manual")
|
|
95
|
+
|
|
96
|
+
return "; ".join(reasons)
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Dream: extração automática de memórias (extractMemories desbloqueado).
|
|
2
|
+
|
|
3
|
+
Replica o fluxo do extractMemories.ts do Claude Code:
|
|
4
|
+
1. Scan de memórias existentes (evita duplicatas)
|
|
5
|
+
2. Contagem de mensagens novas no transcript
|
|
6
|
+
3. Build do prompt de extração
|
|
7
|
+
4. Chamada à API Claude com max 5 turns
|
|
8
|
+
5. Report do que foi escrito
|
|
9
|
+
|
|
10
|
+
O dream é o "extractMemories desbloqueado" — a feature que a Anthropic
|
|
11
|
+
gateou com tengu_passport_quail=false para usuários não-Anthropic.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from src.core.paths import get_auto_mem_path, get_memory_index, MEMORY_INDEX_MAX_LINES
|
|
20
|
+
from src.memdir.scanner import scan_memory_files, format_memory_manifest, MemoryHeader
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DreamResult:
|
|
25
|
+
"""Resultado da extração de memórias.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
written_files: Lista de arquivos criados/atualizados
|
|
29
|
+
memory_dir: Diretório de memória processado
|
|
30
|
+
dry_run: Se True, nada foi modificado
|
|
31
|
+
period_days: Período analisado
|
|
32
|
+
prompt_preview: Preview do prompt usado na extração
|
|
33
|
+
"""
|
|
34
|
+
written_files: List[Path]
|
|
35
|
+
memory_dir: Path
|
|
36
|
+
dry_run: bool
|
|
37
|
+
period_days: int
|
|
38
|
+
new_memories: List[str] = None
|
|
39
|
+
updated_memories: List[str] = None
|
|
40
|
+
prompt_preview: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============================================================================
|
|
44
|
+
# PROMPT DE EXTRAÇÃO — Tradução direta de buildExtractAutoOnlyPrompt()
|
|
45
|
+
# ============================================================================
|
|
46
|
+
|
|
47
|
+
TYPES_SECTION_INDIVIDUAL = """
|
|
48
|
+
## Tipos de Memória (Taxonomia Fechada)
|
|
49
|
+
|
|
50
|
+
Você DEVE classificar cada memória em um destes 4 tipos:
|
|
51
|
+
|
|
52
|
+
### 1. user (Sempre Privado)
|
|
53
|
+
<scope>always private</scope>
|
|
54
|
+
<description>Informações sobre o papel, objetivos, responsabilidades e conhecimento do usuário</description>
|
|
55
|
+
<when_to_save>Quando você aprende detalhes sobre o papel, preferências, responsabilidades ou conhecimento do usuário</when_to_save>
|
|
56
|
+
<how_to_use>Quando seu trabalho deve ser informado pelo perfil ou perspectiva do usuário</how_to_use>
|
|
57
|
+
|
|
58
|
+
### 2. feedback (Padrão: Privado, Team apenas para convenções de projeto)
|
|
59
|
+
<scope>default to private. Save as team only when the guidance is clearly a project-wide convention</scope>
|
|
60
|
+
<description>Orientação que o usuário deu sobre como abordar o trabalho - tanto o que evitar quanto o que continuar fazendo</description>
|
|
61
|
+
<when_to_save>Qualquer momento que o usuário corrigir sua abordagem ("não isso", "não faça X") OU confirmar que uma abordagem funcionou ("isso mesmo", "perfeito, continue assim")</when_to_save>
|
|
62
|
+
<body_structure>Comece com a regra, depois **Why:** (razão) e **How to apply:** (quando/onde aplicar)</body_structure>
|
|
63
|
+
|
|
64
|
+
### 3. project (Privado ou Team, tendência forte para Team)
|
|
65
|
+
<scope>private or team, but strongly bias toward team</scope>
|
|
66
|
+
<description>Informação sobre trabalho em andamento, metas, iniciativas, bugs ou incidentes no projeto que não é derivável do código ou histórico git</description>
|
|
67
|
+
<when_to_save>Quando você aprende quem está fazendo o quê, por quê, ou até quando. Sempre converta datas relativas para absolutas</when_to_save>
|
|
68
|
+
<body_structure>Comece com o fato/decisão, depois **Why:** (motivação) e **How to apply:** (como isso deve moldar suas sugestões)</body_structure>
|
|
69
|
+
|
|
70
|
+
### 4. reference (Geralmente Team)
|
|
71
|
+
<scope>usually team</scope>
|
|
72
|
+
<description>Ponteiros para onde a informação pode ser encontrada em sistemas externos</description>
|
|
73
|
+
<when_to_save>Quando você aprende sobre recursos em sistemas externos e seu propósito</when_to_save>
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
WHAT_NOT_TO_SAVE_SECTION = """
|
|
77
|
+
## O Que NÃO Salvar em Memória
|
|
78
|
+
|
|
79
|
+
- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.
|
|
80
|
+
- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.
|
|
81
|
+
- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.
|
|
82
|
+
- Anything already documented in CLAUDE.md files.
|
|
83
|
+
- Ephemeral task details: in-progress work, temporary state, current conversation context.
|
|
84
|
+
|
|
85
|
+
These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
TRUSTING_RECALL_SECTION = """
|
|
89
|
+
## Before Recommending from Memory
|
|
90
|
+
|
|
91
|
+
A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:
|
|
92
|
+
|
|
93
|
+
- If the memory names a file path: check the file exists.
|
|
94
|
+
- If the memory names a function or flag: grep for it.
|
|
95
|
+
- If the user is about to act on your recommendation (not just asking about history), verify first.
|
|
96
|
+
|
|
97
|
+
"The memory says X exists" is not the same as "X exists now."
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_opener(new_message_count: int, existing_memories: str) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Constrói opener do prompt de extração.
|
|
104
|
+
|
|
105
|
+
Replica opener() de prompts.ts do Claude Code.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
new_message_count: Número de mensagens novas para analisar
|
|
109
|
+
existing_memories: Manifesto de memórias existentes
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
String opener do prompt
|
|
113
|
+
"""
|
|
114
|
+
return f"""You are now acting as the memory extraction subagent. Analyze the most recent ~{new_message_count} messages above and use them to update your persistent memory systems.
|
|
115
|
+
|
|
116
|
+
Available tools: FileRead, Grep, Glob, read-only Bash (ls/find/cat/stat/wc/head/tail and similar), and FileEdit/FileWrite for paths inside the memory directory only. Bash rm is not permitted. All other tools will be denied.
|
|
117
|
+
|
|
118
|
+
You have a limited turn budget. FileEdit requires a prior FileRead of the same file, so the efficient strategy is: turn 1 — issue all FileRead calls in parallel for every file you might update; turn 2 — issue all FileWrite/FileEdit calls in parallel. Do not interleave reads and writes across multiple turns.
|
|
119
|
+
|
|
120
|
+
You MUST only use content from the last ~{new_message_count} messages to update your persistent memories. Do not waste any turns attempting to investigate or verify that content further — no grepping source files, no reading code to confirm a pattern exists, no git commands.
|
|
121
|
+
|
|
122
|
+
{existing_memories}
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_how_to_save_section(memory_dir: Path) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Constrói seção de como salvar memórias.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
memory_dir: Diretório de memória para incluir no prompt
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
String com instruções de como salvar
|
|
135
|
+
"""
|
|
136
|
+
return f"""
|
|
137
|
+
## Como Salvar Memórias
|
|
138
|
+
|
|
139
|
+
### Formato do Arquivo
|
|
140
|
+
|
|
141
|
+
Cada memória é um arquivo .md separado em: {memory_dir}
|
|
142
|
+
|
|
143
|
+
**Frontmatter (obrigatório):**
|
|
144
|
+
```markdown
|
|
145
|
+
---
|
|
146
|
+
name: {{memory name}}
|
|
147
|
+
description: {{one-line description — usada para decidir relevância em conversas futuras, seja específico}}
|
|
148
|
+
type: {{user, feedback, project, reference}}
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
{{memory content — para tipos feedback/project, estruture como: regra/fato, depois **Why:** e **How to apply:**}}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Atualizando MEMORY.md (Índice)
|
|
155
|
+
|
|
156
|
+
O arquivo MEMORY.md é o índice de todas as memórias. Após criar/atualizar uma memória:
|
|
157
|
+
|
|
158
|
+
1. Leia MEMORY.md existente
|
|
159
|
+
2. Adicione uma linha no formato: `- [{{type}}] {{filename}} ({{timestamp}}): {{description}}`
|
|
160
|
+
3. Mantenha o limite de {MEMORY_INDEX_MAX_LINES} linhas máximo
|
|
161
|
+
|
|
162
|
+
### Regras de Escrita
|
|
163
|
+
|
|
164
|
+
- **Sempre** verifique memórias existentes antes de criar novas (evite duplicatas)
|
|
165
|
+
- **Sempre** use frontmatter com name, description, type
|
|
166
|
+
- **Sempre** inclua **Why:** e **How to apply:** para feedback e project
|
|
167
|
+
- **Nunca** salve o que é derivável do código ou git
|
|
168
|
+
- **Priorize** o que é surpreendente ou não-óbvio
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_extract_dream_prompt(
|
|
173
|
+
new_message_count: int,
|
|
174
|
+
existing_memories: str,
|
|
175
|
+
memory_dir: Path,
|
|
176
|
+
skip_index: bool = False,
|
|
177
|
+
) -> List[str]:
|
|
178
|
+
"""
|
|
179
|
+
Constrói prompt completo de extração (dream).
|
|
180
|
+
|
|
181
|
+
Replica buildExtractAutoOnlyPrompt() do Claude Code (prompts.ts).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
new_message_count: Número de mensagens novas para analisar
|
|
185
|
+
existing_memories: Manifesto de memórias existentes
|
|
186
|
+
memory_dir: Diretório de memória (para instruções de path)
|
|
187
|
+
skip_index: Se True, não atualiza MEMORY.md (modo direto)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Lista de seções do prompt (para join com \\n)
|
|
191
|
+
"""
|
|
192
|
+
how_to_save = build_how_to_save_section(memory_dir)
|
|
193
|
+
|
|
194
|
+
sections = [
|
|
195
|
+
build_opener(new_message_count, existing_memories),
|
|
196
|
+
"",
|
|
197
|
+
"If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.",
|
|
198
|
+
"",
|
|
199
|
+
TYPES_SECTION_INDIVIDUAL.strip(),
|
|
200
|
+
"",
|
|
201
|
+
WHAT_NOT_TO_SAVE_SECTION.strip(),
|
|
202
|
+
"",
|
|
203
|
+
how_to_save.strip(),
|
|
204
|
+
"",
|
|
205
|
+
TRUSTING_RECALL_SECTION.strip(),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
return sections
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ============================================================================
|
|
212
|
+
# CONTADOR DE MENSAGENS DO TRANSCRIPT
|
|
213
|
+
# ============================================================================
|
|
214
|
+
|
|
215
|
+
def count_transcript_messages(since_days: int = 7) -> int:
|
|
216
|
+
"""
|
|
217
|
+
Conta mensagens no transcript do Claude Code.
|
|
218
|
+
|
|
219
|
+
Lê ~/.claude/projects/<slug>/*.jsonl e conta mensagens model-visible
|
|
220
|
+
(type=user ou type=assistant) no período especificado.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
since_days: Dias para analisar (default: 7)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Número de mensagens novas
|
|
227
|
+
"""
|
|
228
|
+
memory_dir = get_auto_mem_path()
|
|
229
|
+
projects_dir = memory_dir.parent # ~/.claude/projects/<slug>/
|
|
230
|
+
|
|
231
|
+
if not projects_dir.exists():
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
cutoff = datetime.now() - timedelta(days=since_days)
|
|
235
|
+
message_count = 0
|
|
236
|
+
|
|
237
|
+
# Lê todos os jsonl do diretório
|
|
238
|
+
jsonl_files = list(projects_dir.glob("*.jsonl"))
|
|
239
|
+
|
|
240
|
+
for jsonl_file in jsonl_files:
|
|
241
|
+
try:
|
|
242
|
+
with open(jsonl_file, "r", encoding="utf-8") as f:
|
|
243
|
+
for line in f:
|
|
244
|
+
line = line.strip()
|
|
245
|
+
if not line:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
import json
|
|
249
|
+
try:
|
|
250
|
+
msg = json.loads(line)
|
|
251
|
+
msg_type = msg.get("type", "")
|
|
252
|
+
|
|
253
|
+
# Conta apenas mensagens model-visible
|
|
254
|
+
if msg_type in ["user", "assistant"]:
|
|
255
|
+
# Verifica timestamp se disponível
|
|
256
|
+
ts = msg.get("ts", "")
|
|
257
|
+
if ts:
|
|
258
|
+
try:
|
|
259
|
+
msg_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
260
|
+
if msg_dt.replace(tzinfo=None) < cutoff:
|
|
261
|
+
continue
|
|
262
|
+
except ValueError:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
message_count += 1
|
|
266
|
+
except json.JSONDecodeError:
|
|
267
|
+
continue
|
|
268
|
+
except Exception:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
return message_count
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ============================================================================
|
|
275
|
+
# ORQUESTRADOR PRINCIPAL
|
|
276
|
+
# ============================================================================
|
|
277
|
+
|
|
278
|
+
def run_dream(
|
|
279
|
+
memory_dir: Path = None,
|
|
280
|
+
since_days: int = 7,
|
|
281
|
+
dry_run: bool = False,
|
|
282
|
+
) -> DreamResult:
|
|
283
|
+
"""
|
|
284
|
+
Executa extração automática de memórias (dream).
|
|
285
|
+
|
|
286
|
+
Replica o fluxo do extractMemories.ts do Claude Code:
|
|
287
|
+
1. Scan de memórias existentes
|
|
288
|
+
2. Contagem de mensagens novas
|
|
289
|
+
3. Build do prompt
|
|
290
|
+
4. Chamada à API Claude (max 5 turns) — SIMULADO neste método
|
|
291
|
+
5. Report
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
memory_dir: Diretório de memória (default: auto-detected)
|
|
295
|
+
since_days: Dias para analisar (default: 7)
|
|
296
|
+
dry_run: Se True, apenas simula, não modifica nada
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
DreamResult com arquivos processados
|
|
300
|
+
"""
|
|
301
|
+
if memory_dir is None:
|
|
302
|
+
memory_dir = get_auto_mem_path()
|
|
303
|
+
|
|
304
|
+
# Passo 1: Scan de memórias existentes
|
|
305
|
+
existing = scan_memory_files(memory_dir)
|
|
306
|
+
existing_manifest = format_memory_manifest(existing)
|
|
307
|
+
|
|
308
|
+
# Passo 2: Contagem de mensagens novas
|
|
309
|
+
message_count = count_transcript_messages(since_days)
|
|
310
|
+
|
|
311
|
+
if message_count == 0:
|
|
312
|
+
return DreamResult(
|
|
313
|
+
written_files=[],
|
|
314
|
+
memory_dir=memory_dir,
|
|
315
|
+
dry_run=dry_run,
|
|
316
|
+
period_days=since_days,
|
|
317
|
+
new_memories=[],
|
|
318
|
+
updated_memories=[]
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Passo 3: Build do prompt
|
|
322
|
+
prompt_sections = build_extract_dream_prompt(
|
|
323
|
+
new_message_count=message_count,
|
|
324
|
+
existing_memories=existing_manifest,
|
|
325
|
+
memory_dir=memory_dir,
|
|
326
|
+
)
|
|
327
|
+
full_prompt = "\n".join(prompt_sections)
|
|
328
|
+
|
|
329
|
+
# Passo 4: Chamada à API Claude
|
|
330
|
+
import re
|
|
331
|
+
import os
|
|
332
|
+
|
|
333
|
+
prompt_preview = full_prompt
|
|
334
|
+
|
|
335
|
+
if dry_run:
|
|
336
|
+
return DreamResult(
|
|
337
|
+
written_files=[],
|
|
338
|
+
memory_dir=memory_dir,
|
|
339
|
+
dry_run=dry_run,
|
|
340
|
+
period_days=since_days,
|
|
341
|
+
new_memories=[],
|
|
342
|
+
updated_memories=[],
|
|
343
|
+
prompt_preview=prompt_preview,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Chamada real à API
|
|
347
|
+
try:
|
|
348
|
+
import anthropic
|
|
349
|
+
_api_key = (
|
|
350
|
+
os.environ.get("ANTHROPIC_AUTH_TOKEN")
|
|
351
|
+
or os.environ.get("ANTHROPIC_API_KEY")
|
|
352
|
+
)
|
|
353
|
+
_base_url = os.environ.get("ANTHROPIC_BASE_URL")
|
|
354
|
+
model = (
|
|
355
|
+
os.environ.get("CEREBRO_MODEL")
|
|
356
|
+
or os.environ.get("ANTHROPIC_MODEL")
|
|
357
|
+
or "claude-opus-4-5"
|
|
358
|
+
)
|
|
359
|
+
client = anthropic.Anthropic(
|
|
360
|
+
api_key=_api_key,
|
|
361
|
+
base_url=_base_url,
|
|
362
|
+
)
|
|
363
|
+
response = client.messages.create(
|
|
364
|
+
model=model,
|
|
365
|
+
max_tokens=8096,
|
|
366
|
+
system=full_prompt,
|
|
367
|
+
messages=[{
|
|
368
|
+
"role": "user",
|
|
369
|
+
"content": (
|
|
370
|
+
"Analyze the conversation transcript and extract memories "
|
|
371
|
+
"worth saving. Focus on decisions, feedback, and project facts."
|
|
372
|
+
)
|
|
373
|
+
}]
|
|
374
|
+
)
|
|
375
|
+
response_text = response.content[0].text if response.content else ""
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return DreamResult(
|
|
378
|
+
written_files=[],
|
|
379
|
+
memory_dir=memory_dir,
|
|
380
|
+
dry_run=dry_run,
|
|
381
|
+
period_days=since_days,
|
|
382
|
+
new_memories=[],
|
|
383
|
+
updated_memories=[],
|
|
384
|
+
prompt_preview=f"ERRO na chamada LLM: {e}",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Parse: extrai filenames .md mencionados na resposta
|
|
388
|
+
mentioned = re.findall(r'[\w\-]+\.md', response_text)
|
|
389
|
+
mentioned = list(dict.fromkeys(mentioned)) # dedup mantendo ordem
|
|
390
|
+
|
|
391
|
+
written_files = []
|
|
392
|
+
new_memories = []
|
|
393
|
+
updated_memories = []
|
|
394
|
+
|
|
395
|
+
for fname in mentioned:
|
|
396
|
+
if fname == "MEMORY.md":
|
|
397
|
+
continue
|
|
398
|
+
fpath = memory_dir / fname
|
|
399
|
+
if fpath.exists():
|
|
400
|
+
updated_memories.append(fname)
|
|
401
|
+
written_files.append(fpath)
|
|
402
|
+
else:
|
|
403
|
+
new_memories.append(fname)
|
|
404
|
+
|
|
405
|
+
return DreamResult(
|
|
406
|
+
written_files=written_files,
|
|
407
|
+
memory_dir=memory_dir,
|
|
408
|
+
dry_run=dry_run,
|
|
409
|
+
period_days=since_days,
|
|
410
|
+
new_memories=new_memories,
|
|
411
|
+
updated_memories=updated_memories,
|
|
412
|
+
prompt_preview=prompt_preview,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def generate_dream_report(result: DreamResult) -> str:
|
|
417
|
+
"""
|
|
418
|
+
Gera relatório da extração dream.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
result: DreamResult da execução
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Relatório formatado em markdown
|
|
425
|
+
"""
|
|
426
|
+
lines = [
|
|
427
|
+
"# Dream Report — Extração de Memórias",
|
|
428
|
+
"",
|
|
429
|
+
f"**Período:** últimos {result.period_days} dias",
|
|
430
|
+
f"**Diretório:** {result.memory_dir}",
|
|
431
|
+
f"**Modo:** {'dry-run (nenhuma modificação)' if result.dry_run else 'aplicação direta'}",
|
|
432
|
+
"",
|
|
433
|
+
"## Resumo",
|
|
434
|
+
"",
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
if result.new_memories:
|
|
438
|
+
lines.append("### Novas Memórias")
|
|
439
|
+
lines.append("")
|
|
440
|
+
for mem in result.new_memories:
|
|
441
|
+
lines.append(f"- {mem}")
|
|
442
|
+
lines.append("")
|
|
443
|
+
else:
|
|
444
|
+
lines.append("Nenhuma memória nova criada.")
|
|
445
|
+
lines.append("")
|
|
446
|
+
|
|
447
|
+
if result.updated_memories:
|
|
448
|
+
lines.append("### Memórias Atualizadas")
|
|
449
|
+
lines.append("")
|
|
450
|
+
for mem in result.updated_memories:
|
|
451
|
+
lines.append(f"- {mem}")
|
|
452
|
+
lines.append("")
|
|
453
|
+
|
|
454
|
+
if result.written_files:
|
|
455
|
+
lines.append("### Arquivos Modificados")
|
|
456
|
+
lines.append("")
|
|
457
|
+
for f in result.written_files:
|
|
458
|
+
lines.append(f"- {f}")
|
|
459
|
+
lines.append("")
|
|
460
|
+
|
|
461
|
+
if not result.new_memories and not result.updated_memories and not result.written_files:
|
|
462
|
+
lines.append("Nenhuma mudança foi necessária.")
|
|
463
|
+
lines.append("")
|
|
464
|
+
|
|
465
|
+
return "\n".join(lines)
|