From 960d7c5f8f68f67b82102ba1d7635bee2421a20e Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Wed, 7 Jan 2026 14:09:22 +0530 Subject: [PATCH] docs: improve robustness and fix variable inconsistencies in earnings call notebook --- .../finance/03_Earnings_Call_Analysis.ipynb | 354 +++++++++++------- 1 file changed, 221 insertions(+), 133 deletions(-) diff --git a/cookbook/use_cases/finance/03_Earnings_Call_Analysis.ipynb b/cookbook/use_cases/finance/03_Earnings_Call_Analysis.ipynb index 5d1e8cc7..cf6529bb 100644 --- a/cookbook/use_cases/finance/03_Earnings_Call_Analysis.ipynb +++ b/cookbook/use_cases/finance/03_Earnings_Call_Analysis.ipynb @@ -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",