nlp-intelligence-analyzer / src /fact_extractor.py
morinousagi's picture
Upload 2 files
aec710a verified
Raw
History Blame Contribute Delete
6 kB
"""
fact_extractor.py
Module: Explicit Fact Extraction for SIGMA Intelligence Analyzer
Purpose:
Extract structured, syntactic facts from narrative text.
Uses spaCy transformer pipeline for:
- Named Entity Recognition (NER)
- Dependency parsing
- Sentence segmentation
Output:
- Entities (with labels)
- Structured factual triples:
(subject → action → object)
Design Philosophy:
Conservative extraction.
No speculative inference.
Only syntactically grounded relations.
"""
import spacy
from spacy.cli import download
class FactExtractor:
def __init__(self):
"""
Load spaCy transformer-based English model.
This model includes:
- NER
- POS tagging
- Dependency parsing
"""
#self.nlp = spacy.load("en_core_web_trf") # transformer-based & very heavy
#self.nlp = spacy.load("en_core_web_sm") # lightweight version
try:
self.nlp = spacy.load("en_core_web_sm")
except:
download("en_core_web_sm")
self.nlp = spacy.load("en_core_web_sm")
def extract_entities(self, text):
"""
Extract named entities from text.
Returns:
List[Dict] with:
{
"text": entity string,
"label": entity type (ORG, PERSON, GPE, DATE, etc.)
}
"""
doc = self.nlp(text)
return [
{"text": ent.text, "label": ent.label_}
for ent in doc.ents
]
def extract_structured_facts(self, text):
"""
Extract structured factual triples from text.
Strategy:
- Iterate through sentences
- Identify ROOT verb of each sentence
- Extract:
- Subject (nsubj / nsubjpass)
- Object/complement structures
- Expand subject and object to full phrase spans
Returns: List[Dict]
"""
doc = self.nlp(text)
facts = []
for sent in doc.sents:
for token in sent:
# Identify main predicate (ROOT verb)
# note: some sentences whose ROOT is AUX or NOUN may still not extract perfectly
if token.pos_ == "VERB" and token.dep_ == "ROOT":
subject = None
obj = None
# -----------------------------
# SUBJECT EXTRACTION
# -----------------------------
for child in token.children:
if child.dep_ in ("nsubj", "nsubjpass"):
subject = self._expand_phrase(child)
# -----------------------------
# OBJECT / COMPLEMENT EXTRACTION
# -----------------------------
for child in token.children:
# Direct object / attribute / object predicate
if child.dep_ in ("dobj", "attr", "oprd"):
obj = self._expand_phrase(child)
# Clausal complements
elif child.dep_ in ("ccomp", "xcomp"):
obj = self._expand_phrase(child)
# Remove leading complementizer
if obj.startswith("that "):
obj = obj[5:]
# -----------------------------------
# Attach ROOT-level prep phrases
# -----------------------------------
if obj:
prep_phrases = []
for child in token.children:
if child.dep_ == "prep":
prep_text = self._expand_phrase(child)
# Avoid duplicate attachment
if prep_text not in obj:
prep_phrases.append(prep_text)
if prep_phrases:
obj += " " + " ".join(prep_phrases)
# Detect negation
negated = any(
child.dep_ == "neg"
for child in token.children
)
action = token.lemma_
if negated:
action = f"not {action}"
# -----------------------------
# STORE FACT
# -----------------------------
facts.append({
"sentence": sent.text.strip(),
"subject": subject,
"action": action,
"object": obj
})
return facts
def _expand_phrase(self, token):
"""
Expand token to its full subtree span and normalize whitespace.
Why:
Dependency parsing identifies only head tokens.
We want full noun phrases including modifiers.
Example:
Head token: "division"
Expanded: "The cybersecurity division"
Implementation:
- Collect all tokens in subtree
- Get span from first to last token
"""
subtree = list(token.subtree)
start = subtree[0].i
end = subtree[-1].i + 1
span_text = token.doc[start:end].text
# Normalize whitespace (remove leading/trailing spaces and newlines)
return " ".join(span_text.strip().split())
def extract_sentences(self, text):
"""
Return list of individual sentences.
Useful for:
- Candidate hypothesis generation
- Summarization preprocessing
"""
doc = self.nlp(text)
return [sent.text.strip() for sent in doc.sents]