# Extract Entities from Text (LLM-based)

Diseño original para fuzzygraph.

## Funcion a implementar

### extract_entities_llm

- **Dominio:** datascience
- **Lang:** Python
- **Purity:** impure (llama LLM)
- **Signature:**
  ```python
  def extract_entities_llm(
      text: str,
      entity_schema: list[dict],
      llm_chat_json: Callable[[list[dict]], dict],
      language_instruction: str = "Respond in English.",
  ) -> list[EntityCandidate]:
  ```
- **Parametros:**
  - `text`: chunk de texto a analizar
  - `entity_schema`: lista de tipos con metadata fields, formato:
    ```python
    [
      {"type_ref": "osint_person_go_cybersecurity", "label": "Person",
       "metadata_fields": ["full_name", "alias", "nationality", "dob", "risk_score"]},
      {"type_ref": "osint_domain_go_cybersecurity", "label": "Domain",
       "metadata_fields": ["fqdn", "registrar", "created_date"]},
      ...
    ]
    ```
  - `llm_chat_json`: funcion que recibe messages y retorna dict (inyeccion de dependencia, no acoplado a OpenAI)
  - `language_instruction`: instruccion de idioma para el LLM
- **Retorno:** `list[EntityCandidate]` (ver spec fg_09)
- **Algoritmo:**
  1. Construir system prompt con el schema de entity types:
     ```
     You are an entity extraction expert. Given text, extract all entities
     matching these types. For each entity, provide: name, type_ref,
     attributes (matching the metadata_fields for that type), and a
     confidence score (0.0-1.0).
     
     Entity types:
     - Person (type_ref: osint_person_go_cybersecurity)
       fields: full_name, alias, nationality, dob, risk_score
     - Domain (type_ref: osint_domain_go_cybersecurity)
       fields: fqdn, registrar, created_date
     ...
     
     Output JSON: {"entities": [{"name": "...", "type_ref": "...", "attributes": {...}, "confidence": 0.9}]}
     
     Rules:
     - Only extract entities explicitly mentioned in the text
     - Use the exact type_ref from the schema
     - Leave unknown attributes as null
     - Confidence: 1.0 = explicitly named, 0.7 = strongly implied, 0.5 = weakly implied
     ```
  2. User message: el texto del chunk
  3. Llamar `llm_chat_json([system, user])` → parsear respuesta
  4. Validar: cada entity tiene name y type_ref valido (del schema)
  5. Retornar lista de EntityCandidate
- **Error handling:**
  - JSON invalido del LLM: retornar lista vacia + warning
  - type_ref no en schema: descartar esa entidad
- **Deps:** ninguna especifica (el LLM se inyecta)
- **Error type:** ValueError (schema vacio)
- **Tests:**
  - Texto con entidades claras (personas, dominios)
  - Texto sin entidades (retorna [])
  - LLM retorna JSON mal formado (retorna [] con warning)
  - type_ref invalido en respuesta (se descarta)
  - Confidence se propaga correctamente
