feat(0013): add extract_panel — UI + subprocess + apply (dedupe)
extract_panel.{h,cpp}: panel ImGui dockeable con textarea grande,
boton Extract que lanza enrichers/paste_extract/run.py en un
std::thread aparte (no bloquea UI), tablas editables de entidades y
relaciones propuestas con checkboxes, y boton Apply Selected que
persiste a operations.db con dedupe por (type_ref, name) y por
(from, to, name) en relaciones.
Parser JSON ad-hoc (suficiente para el contrato del enricher) para
no añadir dependencias. Apply usa SQLite directamente (mismo
patron que entity_ops/jobs.cpp).
Anade panel_extract a AppState. La logica apply esta separada de
ImGui para poder testarla en aislamiento desde pytest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1079
File diff suppressed because it is too large
Load Diff
+139
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Panel "Paste & Extract" (issue 0013).
|
||||
//
|
||||
// Textarea grande para pegar texto. Boton Extract lanza el script
|
||||
// `enrichers/paste_extract/run.py` en un hilo aparte (no bloquea UI).
|
||||
// El script devuelve un JSON con entidades y relaciones propuestas (modo
|
||||
// preview — no escribe a operations.db). El panel muestra dos tablas
|
||||
// (entidades / relaciones) con checkboxes; al pulsar "Apply Selected"
|
||||
// se persisten via entity_ops con dedupe por (type_ref, name).
|
||||
//
|
||||
// Threading: una llamada Extract a la vez (extract_busy bool). El hilo
|
||||
// rellena la propuesta tras hacerse el subprocess. Apply corre en el
|
||||
// thread principal y dispara reload del grafo via app.want_reload.
|
||||
//
|
||||
// El enricher esta declarado en `enrichers/paste_extract/manifest.yaml`
|
||||
// pero NO se invoca via el sistema de jobs — el panel lo lanza
|
||||
// directamente. Vivir en `enrichers/` permite que se distribuya y que
|
||||
// el script use el mismo Python runtime resolution que el resto.
|
||||
|
||||
namespace ge {
|
||||
|
||||
struct AppState;
|
||||
|
||||
// Una entidad propuesta por el extractor. Se guarda como string para
|
||||
// poder editarla inline antes del Apply.
|
||||
struct ProposedEntity {
|
||||
std::string tmp_id; // "tmp_0", "tmp_1", ... vinculado a relaciones
|
||||
std::string type_ref; // editable
|
||||
std::string name; // editable
|
||||
std::string source; // "regex" | "hybrid"
|
||||
int start_offset = -1; // span en el texto pegado
|
||||
int end_offset = -1;
|
||||
double confidence = 1.0;
|
||||
std::string metadata_json; // JSON literal (no editable v1)
|
||||
bool selected = true;
|
||||
|
||||
// Buffers mutables para edicion inline en ImGui.
|
||||
char type_buf[64] = {};
|
||||
char name_buf[256] = {};
|
||||
};
|
||||
|
||||
struct ProposedRelation {
|
||||
std::string from_tmp_id;
|
||||
std::string to_tmp_id;
|
||||
std::string name; // ej: "works_at"
|
||||
std::string source; // "hybrid" | ...
|
||||
double confidence = 0.0;
|
||||
bool selected = true;
|
||||
};
|
||||
|
||||
struct ExtractResult {
|
||||
std::vector<ProposedEntity> entities;
|
||||
std::vector<ProposedRelation> relations;
|
||||
std::vector<std::string> layers;
|
||||
std::string error; // vacio si OK
|
||||
std::string stderr_tail;
|
||||
};
|
||||
|
||||
struct ExtractPanelState {
|
||||
// Buffer de texto del textarea. Crece dinamicamente.
|
||||
std::vector<char> text_buf;
|
||||
bool text_initialized = false;
|
||||
|
||||
// Resultado del ultimo Extract (poblado por el worker thread).
|
||||
std::shared_ptr<ExtractResult> result;
|
||||
std::mutex result_mu;
|
||||
std::atomic<bool> busy{false};
|
||||
std::atomic<bool> new_result{false}; // hay resultado fresco
|
||||
|
||||
// Mensaje de status (en el footer) — refrescado por el worker.
|
||||
std::string status;
|
||||
|
||||
// Stats del ultimo apply.
|
||||
int last_apply_entities = 0;
|
||||
int last_apply_relations = 0;
|
||||
int last_apply_dedup = 0;
|
||||
|
||||
// Toggle: ¿usar hybrid (GLiNER/GLiREL) si esta disponible?
|
||||
bool use_hybrid = false;
|
||||
|
||||
// Worker thread; joinable cuando esta vivo.
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
// Configura paths que el worker necesita para invocar Python. Llamar una
|
||||
// vez tras `jobs_init` (re-usa el resolver de Python runtime + paths).
|
||||
void extract_panel_init(const char* enrichers_dir,
|
||||
const char* app_dir,
|
||||
const char* registry_root);
|
||||
|
||||
// Suelta el worker thread si esta corriendo (cancelable). Llamar al
|
||||
// shutdown de la app.
|
||||
void extract_panel_shutdown();
|
||||
|
||||
// Renderiza el panel. Si app.panel_extract es false, retorna sin dibujar.
|
||||
void extract_panel_render(AppState& app);
|
||||
|
||||
// Aplica las entidades/relaciones marcadas como selected al
|
||||
// operations.db indicado. Inserta entidades nuevas con dedupe por
|
||||
// (type_ref, name); reusa el id existente si lo encuentra. Despues
|
||||
// inserta las relaciones cuyos endpoints (mapeados via tmp_id ->
|
||||
// real_id) sean ambos validos.
|
||||
//
|
||||
// Devuelve los conteos en out_added_entities, out_dedup_entities,
|
||||
// out_added_relations. Tolera que algunas relaciones no resuelvan
|
||||
// (out_skipped_relations). El caller decide si setear app.want_reload.
|
||||
//
|
||||
// Esta funcion es testeable en aislamiento (no toca ImGui).
|
||||
bool extract_panel_apply(const char* ops_db_path,
|
||||
const ExtractResult& result,
|
||||
int* out_added_entities,
|
||||
int* out_dedup_entities,
|
||||
int* out_added_relations,
|
||||
int* out_skipped_relations);
|
||||
|
||||
// Helper interno expuesto para tests: parsea el JSON que produce
|
||||
// `enrichers/paste_extract/run.py`. Devuelve true si el parseo es OK.
|
||||
// En error, result.error se rellena.
|
||||
bool extract_panel_parse_result(const std::string& json_text,
|
||||
ExtractResult* result);
|
||||
|
||||
// Spawnea el subprocess Python para extraer. Sincronico (bloquea el
|
||||
// hilo del caller). El panel lo invoca en un std::thread aparte para
|
||||
// no congelar la UI. Expuesto por si los tests quieren llamarlo
|
||||
// directamente (no por ahora — los tests cubren el lado Python via
|
||||
// pytest, y el lado C++ via parse_result + apply).
|
||||
bool extract_panel_run_subprocess(const std::string& text,
|
||||
bool use_hybrid,
|
||||
ExtractResult* out);
|
||||
|
||||
} // namespace ge
|
||||
Reference in New Issue
Block a user