docs: improve robustness and fix variable inconsistencies in earnings call notebook

This commit is contained in:
KaifAhmad1
2026-01-07 14:09:22 +05:30
parent 2489ce72b5
commit 960d7c5f8f
@@ -32,7 +32,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"metadata": {},
"outputs": [
{
@@ -52,7 +52,7 @@
}
],
"source": [
"!pip install -qU semantica docling \n"
"!pip install -qU semantica docling pdfplumber groq\n"
]
},
{
@@ -73,10 +73,6 @@
"from semantica.llms import Groq\n",
"import os\n",
"\n",
"# Set your Groq API key here or as an environment variable\n",
"# Option 1: Set environment variable (recommended): export GROQ_API_KEY=\"your-api-key-here\"\n",
"# Option 2: For Google Colab: from google.colab import userdata; GROQ_API_KEY = userdata.get(\"GROQ_API_KEY\")\n",
"# Option 3: Set directly below (not recommended for production)\n",
"GROQ_API_KEY = os.getenv(\"GROQ_API_KEY\", \"\")\n",
"\n",
"if not GROQ_API_KEY:\n",
@@ -110,7 +106,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"metadata": {},
"outputs": [
{
@@ -154,8 +150,14 @@
" transcript_pdf.write_bytes(requests.get(transcript_url).content)\n",
"\n",
"# Parse documents using DoclingParser\n",
"press_release = parser.parse(press_release_pdf)\n",
"transcript = parser.parse(transcript_pdf)\n",
"try:\n",
" press_release = parser.parse(press_release_pdf)\n",
" transcript = parser.parse(transcript_pdf)\n",
"except Exception as e:\n",
" print(f\"⚠️ Parsing failed: {e}\")\n",
" print(\"Using fallback empty documents for demonstration.\")\n",
" press_release = {\"full_text\": \"\", \"tables\": []}\n",
" transcript = {\"full_text\": \"\", \"tables\": []}\n",
"\n",
"# Combine parsed documents\n",
"parsed_doc = {\n",
@@ -195,24 +197,65 @@
}
],
"source": [
"# Step 2: Normalize text with Semantica\n",
"# Step 2: Normalize full document text (run ONCE)\n",
"from semantica.normalize import TextNormalizer\n",
"# Initialize normalizer\n",
"normalizer = TextNormalizer()\n",
"\n",
"text_normalizer = TextNormalizer()\n",
"normalized_text = text_normalizer.normalize(\n",
"normalized_text = normalizer.normalize(\n",
" parsed_doc[\"full_text\"],\n",
" case=\"lower\",\n",
" remove_extra_whitespace=True\n",
" clean_html=False,\n",
" remove_extra_whitespace=False,\n",
" lowercase=False # preserve casing for entities & finance terms\n",
")\n",
"\n",
"print(f\"Text normalized: {len(normalized_text)} characters\")\n"
"print(f\"Text normalized: {len(normalized_text)} characters\")\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Extract Entities\n",
"## Step 3: Split Text into Chunks\n",
"\n",
"Split the normalized text into overlapping chunks to enable scalable and accurate entity and relation extraction.\n",
"This step prepares the text for LLM-based semantic processing."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.split import TextSplitter\n",
"CHUNK_SIZE = 1000\n",
"CHUNK_OVERLAP = 250\n",
"\n",
"splitter = TextSplitter(\n",
" method=\"recursive\", # safest for long PDFs\n",
" chunk_size=CHUNK_SIZE,\n",
" chunk_overlap=CHUNK_OVERLAP\n",
")\n",
"\n",
"chunks = splitter.split(normalized_text)\n",
"\n",
"print(f\"✓ Created {len(chunks)} chunks\")\n",
"\n",
"# Version-safe access to chunk text\n",
"def get_chunk_text(chunk):\n",
" return getattr(chunk, \"content\", getattr(chunk, \"text\", \"\"))\n",
"\n",
"# Inspect one chunk\n",
"print(\"Sample chunk:\\n\", get_chunk_text(chunks[0])[:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Extract Entities\n",
"\n",
"Extract entities (organizations, people, financial terms) using NERExtractor with Groq LLM.\n"
]
@@ -231,12 +274,12 @@
}
],
"source": [
"# Step 3: Extract entities using NERExtractor with Groq\n",
"# Step 4: Extract entities from ALL chunks using NERExtractor (Groq)\n",
"\n",
"from semantica.semantic_extract import NERExtractor\n",
"import os\n",
"\n",
"text_for_extraction = parsed_doc[\"full_text\"]\n",
"\n",
"# Initialize NER extractor\n",
"ner = NERExtractor(\n",
" method=\"llm\",\n",
" provider=\"groq\",\n",
@@ -252,27 +295,39 @@
" \"LOCATION\", \"GPE\", \"EVENT\", \"QUANTITY\", \"CARDINAL\"\n",
"]\n",
"\n",
"entities = ner.extract_entities(\n",
" text_for_extraction,\n",
" entity_types=entity_types\n",
")\n",
"# Version-safe chunk text accessor\n",
"def get_chunk_text(chunk):\n",
" return getattr(chunk, \"content\", getattr(chunk, \"text\", \"\"))\n",
"\n",
"print(f\"✓ Entities extracted: {len(entities)}\")\n",
"if entities:\n",
" entity_by_type = {}\n",
" for entity in entities:\n",
" entity_by_type.setdefault(entity.label, []).append(entity.text)\n",
" \n",
" print(f\" Entity breakdown:\")\n",
" for label, texts in list(entity_by_type.items())[:5]:\n",
" print(f\" {label}: {len(texts)} entities (e.g., {texts[0]})\")\n"
"all_entities = []\n",
"\n",
"for i, chunk in enumerate(chunks, 1):\n",
" text = get_chunk_text(chunk)\n",
"\n",
" if not text.strip():\n",
" continue\n",
"\n",
" try:\n",
" entities = ner.extract_entities(\n",
" text,\n",
" entity_types=entity_types\n",
" )\n",
" all_entities.extend(entities)\n",
"\n",
" except Exception as e:\n",
" print(f\"⚠️ Chunk {i} failed: {e}\")\n",
"\n",
" if i % 10 == 0 or i == len(chunks):\n",
" print(f\"Processed {i}/{len(chunks)} chunks\")\n",
"\n",
"print(f\"✓ Total entities extracted: {len(all_entities)}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Extract Financial Metrics\n",
"## Step 5: Extract Financial Metrics\n",
"\n",
"Extract financial metrics (money, percentages, dates) from text and tables.\n"
]
@@ -292,30 +347,60 @@
}
],
"source": [
"# Step 4: Extract financial metrics using NERExtractor\n",
"financial_entities = ner.extract_entities(\n",
" text_for_extraction,\n",
" entity_types=[\"MONEY\", \"CURRENCY\", \"PERCENT\", \"PERCENTAGE\", \"QUANTITY\", \"CARDINAL\"]\n",
")\n",
"# Step 5: Extract financial metrics from ALL chunks (explicit + safe)\n",
"\n",
"financial_entity_types = [\n",
" \"MONEY\", \"CURRENCY\", \"PERCENT\", \"PERCENTAGE\",\n",
" \"QUANTITY\", \"CARDINAL\"\n",
"]\n",
"\n",
"def get_chunk_text(chunk):\n",
" return getattr(chunk, \"content\", getattr(chunk, \"text\", \"\"))\n",
"\n",
"financial_entities = []\n",
"\n",
"total_chunks = len(chunks)\n",
"print(f\"Processing {total_chunks} chunks for financial entities...\")\n",
"\n",
"for idx, chunk in enumerate(chunks, start=1):\n",
" text = get_chunk_text(chunk)\n",
"\n",
" # Skip empty chunks but still count them\n",
" if not text.strip():\n",
" print(f\" Skipping empty chunk {idx}/{total_chunks}\")\n",
" continue\n",
"\n",
" # ALWAYS run NER per chunk\n",
" entities = ner.extract_entities(\n",
" text,\n",
" entity_types=financial_entity_types\n",
" )\n",
"\n",
" financial_entities.extend(entities)\n",
"\n",
" if idx % 10 == 0 or idx == total_chunks:\n",
" print(f\" Processed {idx}/{total_chunks} chunks\")\n",
"\n",
"# Aggregate results\n",
"financial_metrics = {\"money\": [], \"percentages\": [], \"quantities\": []}\n",
"for entity in financial_entities:\n",
" label_lower = entity.label.lower()\n",
" if \"money\" in label_lower or \"currency\" in label_lower:\n",
" financial_metrics[\"money\"].append(entity.text)\n",
" elif \"percent\" in label_lower:\n",
" financial_metrics[\"percentages\"].append(entity.text)\n",
" elif \"quantity\" in label_lower or \"cardinal\" in label_lower:\n",
" financial_metrics[\"quantities\"].append(entity.text)\n",
"\n",
"financial_metrics_flat = {v: v for category in financial_metrics.values() for v in category}\n",
"for e in financial_entities:\n",
" label = e.label.lower()\n",
" if \"money\" in label or \"currency\" in label:\n",
" financial_metrics[\"money\"].append(e.text)\n",
" elif \"percent\" in label:\n",
" financial_metrics[\"percentages\"].append(e.text)\n",
" elif \"quantity\" in label or \"cardinal\" in label:\n",
" financial_metrics[\"quantities\"].append(e.text)\n",
"\n",
"print(f\"✓ Financial entities: {len(financial_entities)}\")\n",
"print(f\"✓ Financial entity mentions extracted: {len(financial_entities)}\")\n",
"print(f\" Money/Currency: {len(financial_metrics['money'])}\")\n",
"print(f\" Percentages: {len(financial_metrics['percentages'])}\")\n",
"print(f\" Quantities: {len(financial_metrics['quantities'])}\")\n",
"\n",
"if financial_entities:\n",
" print(f\" Sample: {financial_entities[0].text} ({financial_entities[0].label})\")\n"
" sample = financial_entities[0]\n",
" print(f\" Sample: {sample.text} ({sample.label})\")\n"
]
},
{
@@ -341,49 +426,50 @@
}
],
"source": [
"# Step 5: Extract relationships using RelationExtractor with Groq LLM\n",
"# Step 6: Extract relationships from all chunks (minimal handling)\n",
"\n",
"from semantica.semantic_extract import RelationExtractor\n",
"import os\n",
"\n",
"if not entities:\n",
" print(\"⚠️ No entities found. Skipping relationship extraction.\")\n",
" relationships = []\n",
"else:\n",
" relation_extractor = RelationExtractor(\n",
" method=\"llm\",\n",
" confidence_threshold=0.5,\n",
" relation_types=[\n",
" \"HAS_REVENUE\", \"HAS_EPS\", \"HAS_MARGIN\", \"HAS_PROFIT\", \"HAS_GROWTH\",\n",
" \"PROVIDES_GUIDANCE\", \"STATES\", \"ANNOUNCES\", \"REPORTS\", \"EXPECTS\",\n",
" \"OPERATES_IN\", \"LOCATED_IN\", \"PARTNERS_WITH\", \"SERVES\",\n",
" \"COMPARED_TO\", \"INCREASED_BY\", \"DECREASED_BY\", \"CHANGED_BY\",\n",
" \"DURING\", \"IN_QUARTER\", \"FOR_PERIOD\",\n",
" \"RELATED_TO\", \"PART_OF\", \"AFFECTS\"\n",
" ],\n",
" api_key=os.getenv(\"GROQ_API_KEY\")\n",
"relation_extractor = RelationExtractor(\n",
" method=\"llm\",\n",
" confidence_threshold=0.5,\n",
" relation_types=[\n",
" \"HAS_REVENUE\", \"HAS_EPS\", \"HAS_MARGIN\", \"HAS_PROFIT\", \"HAS_GROWTH\",\n",
" \"PROVIDES_GUIDANCE\", \"STATES\", \"ANNOUNCES\", \"REPORTS\", \"EXPECTS\",\n",
" \"OPERATES_IN\", \"LOCATED_IN\", \"PARTNERS_WITH\", \"SERVES\",\n",
" \"COMPARED_TO\", \"INCREASED_BY\", \"DECREASED_BY\", \"CHANGED_BY\",\n",
" \"DURING\", \"IN_QUARTER\", \"FOR_PERIOD\",\n",
" \"RELATED_TO\", \"PART_OF\", \"AFFECTS\"\n",
" ],\n",
" api_key=os.getenv(\"GROQ_API_KEY\")\n",
")\n",
"\n",
"def get_chunk_text(chunk):\n",
" return getattr(chunk, \"content\", getattr(chunk, \"text\", \"\"))\n",
"\n",
"relationships = []\n",
"\n",
"for chunk in chunks:\n",
" text = get_chunk_text(chunk)\n",
" if not text.strip():\n",
" continue\n",
"\n",
" relationships.extend(\n",
" relation_extractor.extract_relations(\n",
" text,\n",
" entities=all_entities,\n",
" provider=\"groq\",\n",
" llm_model=\"llama-3.1-8b-instant\",\n",
" temperature=0.0\n",
" )\n",
" )\n",
"\n",
" relationships = relation_extractor.extract_relations(\n",
" text_for_extraction,\n",
" entities=entities,\n",
" provider=\"groq\",\n",
" llm_model=\"llama-3.1-8b-instant\",\n",
" temperature=0.0\n",
" )\n",
"print(f\"✓ Relationships extracted: {len(relationships)}\")\n",
"\n",
" print(f\"✓ Relationships extracted: {len(relationships)}\")\n",
" if relationships:\n",
" rel_by_type = {}\n",
" for rel in relationships:\n",
" rel_by_type.setdefault(rel.predicate, []).append(rel)\n",
" \n",
" print(f\" Relationship breakdown:\")\n",
" for pred, rels in list(rel_by_type.items())[:5]:\n",
" sample = rels[0]\n",
" print(f\" {pred}: {len(rels)} relationships\")\n",
" print(f\" Sample: {sample.subject.text} → {sample.predicate} → {sample.object.text}\")\n",
" else:\n",
" print(\" ⚠️ No relationships extracted.\")\n"
"if relationships:\n",
" sample = relationships[0]\n",
" print(f\"Sample: {sample.subject.text} → {sample.predicate} → {sample.object.text}\")\n"
]
},
{
@@ -427,49 +513,51 @@
}
],
"source": [
"# Step 6: Extract RDF triplets using TripletExtractor with Groq LLM\n",
"# Step 7: Extract RDF triplets from all chunks (minimal handling)\n",
"\n",
"from semantica.semantic_extract import TripletExtractor\n",
"import os\n",
"\n",
"if not entities:\n",
" print(\"⚠️ No entities found. Skipping triplet extraction.\")\n",
" triplets = []\n",
" validated_triplets = []\n",
"else:\n",
" triplet_extractor = TripletExtractor(\n",
" method=\"llm\",\n",
" include_temporal=True,\n",
" include_provenance=True,\n",
" provider=\"groq\",\n",
" llm_model=\"llama-3.1-8b-instant\",\n",
" temperature=0.0,\n",
" api_key=os.getenv(\"GROQ_API_KEY\")\n",
"triplet_extractor = TripletExtractor(\n",
" method=\"llm\",\n",
" include_temporal=True,\n",
" include_provenance=True,\n",
" provider=\"groq\",\n",
" llm_model=\"llama-3.1-8b-instant\",\n",
" temperature=0.0,\n",
" api_key=os.getenv(\"GROQ_API_KEY\")\n",
")\n",
"\n",
"def get_chunk_text(chunk):\n",
" return getattr(chunk, \"content\", getattr(chunk, \"text\", \"\"))\n",
"\n",
"triplets = []\n",
"\n",
"for chunk in chunks:\n",
" text = get_chunk_text(chunk)\n",
" if not text.strip():\n",
" continue\n",
"\n",
" triplets.extend(\n",
" triplet_extractor.extract_triplets(\n",
" text,\n",
" entities=all_entities,\n",
" relations=relationships if relationships else None\n",
" )\n",
" )\n",
"\n",
" triplets = triplet_extractor.extract_triplets(\n",
" text_for_extraction,\n",
" entities=entities,\n",
" relations=relationships if relationships else None\n",
" )\n",
"# Optional validation (if available)\n",
"validated_triplets = (\n",
" triplet_extractor.validate_triplets(triplets)\n",
" if hasattr(triplet_extractor, \"validate_triplets\")\n",
" else triplets\n",
")\n",
"\n",
" if hasattr(triplet_extractor, 'triplet_validator'):\n",
" validated_triplets = triplet_extractor.triplet_validator.validate_triplets(triplets)\n",
" else:\n",
" validated_triplets = triplets\n",
"print(f\"✓ RDF triplets extracted: {len(validated_triplets)}\")\n",
"\n",
" print(f\"✓ RDF triplets extracted: {len(triplets)}\")\n",
" if triplets:\n",
" triplet_by_pred = {}\n",
" for t in triplets:\n",
" triplet_by_pred.setdefault(t.predicate, []).append(t)\n",
" \n",
" print(f\" Triplet breakdown:\")\n",
" for pred, ts in list(triplet_by_pred.items())[:5]:\n",
" sample = ts[0]\n",
" print(f\" {pred}: {len(ts)} triplets\")\n",
" print(f\" Sample: {sample.subject} → {sample.predicate} → {sample.object}\")\n",
" else:\n",
" print(\" ⚠️ No triplets extracted.\")\n"
"if validated_triplets:\n",
" t = validated_triplets[0]\n",
" print(f\"Sample: {t.subject} → {t.predicate} → {t.object}\")"
]
},
{
@@ -497,7 +585,7 @@
")\n",
"\n",
"# Track sources for entities\n",
"for entity in entities:\n",
"for entity in all_entities:\n",
" entity_id = getattr(entity, 'id', None) or getattr(entity, 'text', '')\n",
" entity_name = getattr(entity, 'text', '')\n",
" source_tracker.track_property_source(\n",
@@ -513,7 +601,7 @@
"\n",
"# Detect value conflicts\n",
"value_conflicts = conflict_detector.detect_value_conflicts(\n",
" [{'id': getattr(e, 'id', ''), 'name': getattr(e, 'text', '')} for e in entities],\n",
" [{'id': getattr(e, 'id', ''), 'name': getattr(e, 'text', '')} for e in all_entities],\n",
" property_name='name'\n",
")\n",
"\n",
@@ -549,7 +637,7 @@
")\n",
"\n",
"# Resolve value conflicts\n",
"resolved_entities = list(entities)\n",
"resolved_entities = list(all_entities)\n",
"resolved_conflicts = []\n",
"for conflict in value_conflicts:\n",
" resolution = conflict_resolver.resolve_conflict(conflict, strategy='voting')\n",
@@ -641,7 +729,7 @@
"\n",
"# Convert triplets to relationships format\n",
"triplet_relationships = []\n",
"for triplet in triplets:\n",
"for triplet in validated_triplets:\n",
" triplet_relationships.append({\n",
" \"source\": triplet.subject,\n",
" \"predicate\": triplet.predicate,\n",
@@ -655,7 +743,7 @@
"kg_data = {\n",
" \"entities\": merged_entities,\n",
" \"relationships\": all_relationships,\n",
" \"triplets\": triplets,\n",
" \"triplets\": validated_triplets,\n",
" \"metadata\": {\n",
" \"source\": \"earnings_call_transcript\",\n",
" \"financial_metrics\": financial_metrics,\n",
@@ -900,7 +988,7 @@
"memory_ids = []\n",
"memory_contents = [\n",
" f\"Earnings call transcript: {parsed_doc['metadata'].get('title', 'Q1 2024')}\",\n",
" f\"Financial metrics extracted: {len(financial_metrics)} metrics\",\n",
" f\"Financial metrics extracted: {sum(len(v) for v in financial_metrics.values())} metrics\",\n",
" f\"Key entities identified: {len(merged_entities)} entities\"\n",
"]\n",
"\n",
@@ -1098,11 +1186,11 @@
"analysis_summary = {\n",
" \"financial_metrics\": financial_metrics,\n",
" \"extraction_stats\": {\n",
" \"entities\": len(entities),\n",
" \"relationships\": len(relationships),\n",
" \"triplets\": len(triplets),\n",
" \"provider\": f\"Groq LLM (semantica.llms module) - {groq_llm.model}\"\n",
" },\n",
1189→ " \"entities\": len(all_entities),\n",
1190→ " \"relationships\": len(relationships),\n",
1191→ " \"triplets\": len(triplets),\n",
1192→ " \"provider\": f\"Groq LLM (semantica.llms module) - {groq_llm.model}\"\n",
1193→ " },\n",
" \"conflict_resolution\": {\n",
" \"conflicts_detected\": len(value_conflicts) + len(relationship_conflicts),\n",
" \"conflicts_resolved\": len(resolved_conflicts),\n",