Sam commited on
Commit
3becca3
·
verified ·
1 Parent(s): 9a8bcb4

Create yp.s resopmocC

Browse files
aduc_framework/engineers/yp.s resopmocC ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # aduc_framework/engineers/composer.py
2
+ #
3
+ # Copyright (C) August 4, 2025 Carlos Rodrigues dos Santos
4
+ #
5
+ # Versão 2.0.0 (Agnostic & Conversational Execution Engine)
6
+ #
7
+ # O Composer é o Mestre de Obras. Ele executa um plano de pré-produção,
8
+ # construindo o DNA Bruto através de uma conversa com o LLM, orquestrada
9
+ # de forma agnóstica ao modelo através do PromptEngine.
10
+
11
+ import logging
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+ from PIL import Image
16
+ from typing import List, Dict, Any, Generator, Optional, Callable
17
+
18
+ # Importa os componentes com os quais ele interage
19
+ from .prompt_engine import prompt_engine_singleton
20
+ from ..managers.llama_multimodal_manager import llama_multimodal_manager_singleton
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ def robust_json_parser(raw_text: str) -> dict:
25
+ """
26
+ Tenta extrair e parsear um objeto JSON de uma string que pode conter
27
+ texto adicional, explicações ou blocos de código markdown.
28
+ """
29
+ logger.debug(f"Tentando parsear JSON do texto bruto (primeiros 500 chars):\n---\n{raw_text[:500]}\n---")
30
+ # Padrão para extrair de um bloco de código ```json ... ```
31
+ match = re.search(r'```json\s*(\{.*?\})\s*```', raw_text, re.DOTALL)
32
+ if match:
33
+ json_str = match.group(1)
34
+ logger.debug("Bloco de código JSON explícito encontrado, parseando...")
35
+ return json.loads(json_str)
36
+
37
+ # Se não, tenta encontrar o primeiro '{' e o último '}' no texto
38
+ try:
39
+ start_index = raw_text.find('{')
40
+ end_index = raw_text.rfind('}')
41
+ if start_index != -1 and end_index != -1 and end_index > start_index:
42
+ json_str = raw_text[start_index : end_index + 1]
43
+ logger.debug("JSON encontrado por delimitadores '{...}', parseando...")
44
+ return json.loads(json_str)
45
+ except json.JSONDecodeError:
46
+ pass # Ignora e tenta a abordagem final
47
+
48
+ # Como último recurso, tenta parsear o texto inteiro
49
+ logger.debug("Nenhum delimitador ou bloco de código encontrado, tentando parsear o texto inteiro...")
50
+ return json.loads(raw_text)
51
+
52
+ class Composer:
53
+ """
54
+ O Composer executa um plano de trabalho, supervisionando o LLM para
55
+ realizar cada tarefa e construir o DNA de pré-produção de forma iterativa.
56
+ """
57
+
58
+ def __init__(self):
59
+ """
60
+ Inicializa o Composer carregando os templates de tarefa genéricos.
61
+ """
62
+ self.task_templates = self._load_task_templates()
63
+ logger.info(f"Composer inicializado com {len(self.task_templates)} templates de tarefa.")
64
+
65
+ def _load_task_templates(self) -> Dict[str, str]:
66
+ """Carrega todos os templates de tarefa genéricos da pasta 'task_templates'."""
67
+ templates = {}
68
+ template_dir = Path(__file__).resolve().parent.parent / "prompts" / "task_templates"
69
+ if not template_dir.is_dir():
70
+ raise FileNotFoundError(f"Diretório de templates de tarefa não encontrado: {template_dir}")
71
+
72
+ for task_file in template_dir.glob("*.txt"):
73
+ task_id = task_file.stem
74
+ with open(task_file, 'r', encoding='utf-8') as f:
75
+ templates[task_id] = f.read()
76
+ return templates
77
+
78
+ def _talk_to_llama(self, generic_prompt: str, images: Optional[List[Image.Image]] = None, expected_format="text") -> Any:
79
+ """
80
+ Wrapper completo para o ciclo de comunicação: Tradução + Execução.
81
+ """
82
+ # 1. Traduz o prompt genérico para o formato específico do modelo
83
+ final_model_prompt = prompt_engine_singleton.translate(generic_prompt)
84
+
85
+ # 2. Loga a entrada para a UI
86
+ log_entry_request = {
87
+ "direction": "to_llama", "prompt": final_model_prompt,
88
+ "image_count": len(images) if images else 0, "expected_format": expected_format
89
+ }
90
+ logger.info(f"==> ENVIANDO TAREFA PARA O LLAMA (Esperando {expected_format}):\n{final_model_prompt[:400]}...")
91
+
92
+ # 3. Executa a chamada ao LLM
93
+ response_raw = llama_multimodal_manager_singleton.process_turn(
94
+ prompt_text=final_model_prompt, image_list=images
95
+ )
96
+
97
+ # 4. Loga a saída para a UI
98
+ log_entry_response = {"direction": "from_llama", "raw_response": response_raw}
99
+ logger.info(f"<== RESPOSTA BRUTA RECEBIDA DO LLAMA:\n{response_raw[:400]}...")
100
+
101
+ # 5. Processa a saída
102
+ if expected_format == "json":
103
+ try:
104
+ parsed_json = robust_json_parser(response_raw)
105
+ return parsed_json
106
+ except (json.JSONDecodeError, ValueError) as e:
107
+ logger.error(f"Falha ao parsear JSON da resposta do LLAMA. Resposta bruta: {response_raw}", exc_info=True)
108
+ raise ValueError(f"O LLM retornou um formato JSON inválido. Erro: {e}")
109
+ return response_raw
110
+
111
+ def execute_plan(
112
+ self,
113
+ execution_plan: List[Dict[str, Any]],
114
+ initial_data: Dict[str, Any],
115
+ callback: Optional[Callable] = None
116
+ ) -> Generator[Dict[str, Any], None, Dict[str, Any]]:
117
+ """
118
+ Executa um plano de trabalho tarefa por tarefa, emitindo atualizações
119
+ de progresso e construindo o DNA.
120
+ """
121
+ llama_multimodal_manager_singleton.reset_chat()
122
+
123
+ dna = {
124
+ "global_prompt": initial_data["global_prompt"],
125
+ "initial_media_paths": initial_data["user_media_paths"],
126
+ "asset_catalog": {}, "story_summary": "", "scenes": []
127
+ }
128
+ user_media = [Image.open(p) for p in initial_data["user_media_paths"]]
129
+ total_tasks = len(execution_plan)
130
+
131
+ for i, task in enumerate(execution_plan):
132
+ task_id = task['task_id']
133
+ progress_fraction = (i + 0.1) / total_tasks
134
+ message = f"({i+1}/{total_tasks}) {task['description']}"
135
+ if callback: callback(progress_fraction, desc=message)
136
+ yield {"status": "progress", "progress": progress_fraction, "message": message, "dna_snapshot": dna}
137
+
138
+ generic_template = self.task_templates.get(task_id)
139
+ if not generic_template:
140
+ logger.warning(f"Nenhum template encontrado para a tarefa '{task_id}'. Pulando.")
141
+ continue
142
+
143
+ # Prepara os dados para o template, incluindo o estado atual do DNA
144
+ template_data = {**task['inputs'], **dna}
145
+
146
+ # --- Lógica de Execução de Tarefas Específicas ---
147
+ if task_id == "PREPROD_01_CATALOG_ASSETS":
148
+ prompt = generic_template.format(**template_data)
149
+ # A primeira tarefa envia todas as mídias do usuário
150
+ asset_catalog = self._talk_to_llama(prompt, user_media, "json")
151
+ dna["asset_catalog"] = asset_catalog
152
+
153
+ elif task_id == "PREPROD_02_SCORE_ASSETS":
154
+ prompt = generic_template.format(asset_catalog_str=json.dumps(dna["asset_catalog"], indent=2), **template_data)
155
+ scored_catalog = self._talk_to_llama(prompt, expected_format="json")
156
+ dna["asset_catalog"] = scored_catalog
157
+
158
+ elif task_id == "PREPROD_03_CREATE_SUMMARY":
159
+ prompt = generic_template.format(**template_data)
160
+ summary = self._talk_to_llama(prompt, expected_format="text")
161
+ dna["story_summary"] = summary
162
+
163
+ elif task_id == "PREPROD_04_FRAGMENT_SCENES":
164
+ prompt = generic_template.format(
165
+ asset_catalog_str=json.dumps(dna["asset_catalog"], indent=2),
166
+ **template_data
167
+ )
168
+ scenes_data = self._talk_to_llama(prompt, expected_format="json")
169
+ dna["scenes"] = scenes_data.get("scenes", [])
170
+
171
+ elif task_id == "PREPROD_05_REVIEW_PLAN":
172
+ prompt = generic_template.format(scenes_str=json.dumps(dna["scenes"], indent=2))
173
+ review = self._talk_to_llama(prompt, expected_format="json")
174
+ if review.get("changes_needed", False):
175
+ dna["scenes"] = review.get("updated_scenes", dna["scenes"])
176
+ logger.info("Plano de cenas foi ajustado pelo LLM após revisão.")
177
+ else:
178
+ logger.info("LLM confirmou a coerência do plano de cenas.")
179
+
180
+ elif task_id == "PREPROD_06_FRAGMENT_ACTS":
181
+ prompt = generic_template.format(
182
+ scenes_str=json.dumps(dna["scenes"], indent=2),
183
+ **template_data
184
+ )
185
+ final_plan = self._talk_to_llama(prompt, expected_format="json")
186
+ dna["scenes"] = final_plan.get("scenes_with_acts", [])
187
+
188
+ task['status'] = 'complete'
189
+ progress_fraction = (i + 1) / total_tasks
190
+ if callback: callback(progress_fraction, desc=f"({i+1}/{total_tasks}) Tarefa Concluída!")
191
+ yield {"status": "progress", "progress": progress_fraction, "message": f"Tarefa '{task_id}' concluída.", "dna_snapshot": dna}
192
+
193
+ final_message = "Plano de pré-produção concluído com sucesso."
194
+ logger.info(final_message)
195
+ yield {"status": "complete", "progress": 1.0, "message": final_message, "dna": dna}
196
+ return dna
197
+
198
+ # --- Instância Singleton ---
199
+ # A seleção do mapa de modelo é feita no PromptEngine, tornando o Composer agnóstico.
200
+ composer_singleton = Composer()