fdc6b91f4d
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>
140 lines
5.4 KiB
C++
140 lines
5.4 KiB
C++
#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
|