feat: Intelligence Pipeline v3 — full implementation

Multi-stage evidence-grounded inference architecture replacing the
monolithic 9B model extraction pipeline. CPU-first specialist services
handle routine extraction while the 9B vLLM model is preserved for
semantic adjudication of ambiguous cases.

Key components:
- Capability-aware inference gateway (OpenAI-compatible + Ollama)
- Endpoint registry with DB migrations and REST API
- Sentence-aware document segmenter (property tests)
- Deterministic financial parsing with offset integrity
- Symbol resolution with ambiguity detection
- Specialist service (GLiNER2, dynamic batching, K8s deployment)
- Company-specific sentiment (FinBERT, calibration)
- Retrieval-based novelty and duplicate detection
- Confidence calibration pipeline
- Deterministic routing engine (property tests)
- 9B adjudication layer with VRAM gating
- Stock-specific impact model (features, labels, baseline, trained)
- Pipeline orchestrator (state machine, queues, leases, feature flags)
- Bounded parallelism (async workers, semaphore, load shedding)
- Observability (tracing, metrics, alerts)
- Compatibility adapter (v3→v2 golden mapping tests)
- Shadow/canary promotion framework
- Active learning and fine-tuning pipeline

Test results: 1,161 tests pass, ruff lint clean.
All 282 spec tasks completed.
This commit is contained in:
Celes Renata
2026-07-13 02:14:59 +00:00
parent 84634a365e
commit a72f336ad1
227 changed files with 50403 additions and 0 deletions
@@ -0,0 +1,352 @@
"""Sentence-aware document segmenter.
Splits documents into chunks that respect sentence boundaries, preserve
source character offsets, and use document-type-specific strategies.
Key invariant: chunk.text == source_text[chunk.start_char:chunk.end_char]
"""
from __future__ import annotations
import re
from services.intelligence_pipeline_v3.segmenter.boilerplate import score_boilerplate
from services.intelligence_pipeline_v3.segmenter.models import DocumentChunk
from services.intelligence_pipeline_v3.segmenter.strategies import (
ChunkStrategy,
get_strategy,
)
# Sentence-ending patterns: period/question/exclamation followed by space or newline
_SENTENCE_END = re.compile(r"(?<=[.!?])\s+")
# Speaker turn pattern for transcripts
_SPEAKER_PATTERN = re.compile(
r"^([A-Z][a-zA-Z\s\-\.]+(?:\s*[-–—]\s*[A-Za-z\s,]+)?)\s*:\s*",
re.MULTILINE,
)
# Filing section headers
_FILING_SECTION_PATTERN = re.compile(
r"^((?:Item\s+\d+[A-Z]?[\.\:]|PART\s+[IVX]+)[^\n]*)",
re.IGNORECASE | re.MULTILINE,
)
class Segmenter:
"""Sentence-aware document segmenter with document-type-specific strategies.
Does NOT truncate documents — processes full text regardless of length.
Each chunk preserves exact character offsets into the source document.
"""
def segment(
self,
text: str,
document_type: str,
document_id: str = "",
) -> list[DocumentChunk]:
"""Segment a document into chunks.
Args:
text: Full document text (no truncation applied).
document_type: Type of document for strategy selection.
document_id: Identifier for deterministic chunk IDs.
Returns:
List of DocumentChunk with valid offsets and checksums.
"""
if not text:
return []
strategy = get_strategy(document_type)
# Get structural boundaries based on document type
boundaries = self._find_boundaries(text, strategy, document_type)
# Build chunks respecting sentence boundaries and strategy limits
chunks = self._build_chunks(
text=text,
boundaries=boundaries,
strategy=strategy,
document_type=document_type,
document_id=document_id,
)
# Apply overlap between adjacent chunks
chunks = self._apply_overlap(chunks, text, strategy, document_id, document_type)
# Score boilerplate
for chunk in chunks:
chunk.boilerplate_score = score_boilerplate(chunk.text)
# Apply section path and speaker metadata
self._apply_metadata(chunks, text, document_type)
return chunks
def _find_boundaries(
self,
text: str,
strategy: ChunkStrategy,
document_type: str,
) -> list[int]:
"""Find structural boundary positions in the text.
Returns sorted list of character offsets where structural breaks occur.
"""
boundaries: set[int] = set()
for pattern in strategy.boundary_patterns:
for match in pattern.finditer(text):
boundaries.add(match.start())
return sorted(boundaries)
def _find_sentence_boundaries(self, text: str, start: int, end: int) -> list[int]:
"""Find sentence-ending positions within a text range.
Returns character offsets (relative to full document) after sentence-ending punctuation.
"""
segment = text[start:end]
positions: list[int] = []
for match in _SENTENCE_END.finditer(segment):
positions.append(start + match.start())
return positions
def _build_chunks(
self,
text: str,
boundaries: list[int],
strategy: ChunkStrategy,
document_type: str,
document_id: str,
) -> list[DocumentChunk]:
"""Build initial chunks from text using boundaries and sentence awareness."""
chunks: list[DocumentChunk] = []
text_len = len(text)
pos = 0
while pos < text_len:
# Determine the ideal end position
ideal_end = min(pos + strategy.target_chars, text_len)
max_end = min(pos + strategy.max_chars, text_len)
if ideal_end >= text_len:
# Last chunk — take everything remaining
chunk_end = text_len
else:
# Try to break at a structural boundary between ideal and max
chunk_end = self._find_best_break(
text, pos, ideal_end, max_end, boundaries, strategy
)
chunk_text = text[pos:chunk_end]
# Skip empty chunks (shouldn't happen, but defensive)
if not chunk_text.strip():
pos = chunk_end
continue
chunk = DocumentChunk(
chunk_id=f"{document_id}:{pos}",
document_id=document_id,
document_type=document_type,
section_path=[],
speaker=None,
start_char=pos,
end_char=chunk_end,
text=chunk_text,
overlap_left=0,
overlap_right=0,
boilerplate_score=0.0,
)
chunks.append(chunk)
pos = chunk_end
return chunks
def _find_best_break(
self,
text: str,
start: int,
ideal_end: int,
max_end: int,
boundaries: list[int],
strategy: ChunkStrategy,
) -> int:
"""Find the best break point between ideal_end and max_end.
Priority:
1. Structural boundary nearest to ideal_end (within target..max range)
2. Sentence boundary nearest to ideal_end
3. Whitespace nearest to ideal_end
4. Hard cut at ideal_end
"""
# Look for structural boundaries in the window [ideal_end - target_chars/4, max_end]
search_start = max(start, ideal_end - strategy.target_chars // 4)
best_structural = None
for b in boundaries:
if search_start <= b <= max_end and b > start:
if best_structural is None or abs(b - ideal_end) < abs(best_structural - ideal_end):
best_structural = b
if best_structural is not None:
return best_structural
# Look for sentence boundaries near ideal_end
sentence_breaks = self._find_sentence_boundaries(text, search_start, max_end)
if sentence_breaks:
# Pick the one closest to ideal_end
best_sentence = min(sentence_breaks, key=lambda s: abs(s - ideal_end))
# Use position after the sentence-ending whitespace
after = best_sentence
while after < max_end and text[after] in " \t\n\r":
after += 1
return after
# Fall back to whitespace break
search_region = text[ideal_end:max_end]
ws_match = re.search(r"\s+", search_region)
if ws_match:
return ideal_end + ws_match.end()
# Hard cut
return ideal_end
def _apply_overlap(
self,
chunks: list[DocumentChunk],
text: str,
strategy: ChunkStrategy,
document_id: str,
document_type: str,
) -> list[DocumentChunk]:
"""Apply overlap between adjacent chunks by extending start/end.
Overlap is achieved by moving each chunk's start_char backward
to include trailing content from the previous chunk.
"""
if len(chunks) <= 1 or strategy.overlap_chars == 0:
return chunks
result: list[DocumentChunk] = []
for i, chunk in enumerate(chunks):
new_start = chunk.start_char
new_end = chunk.end_char
overlap_left = 0
overlap_right = 0
if i > 0:
# Extend start backward for overlap
overlap_target = min(strategy.overlap_chars, chunk.start_char)
new_start = max(0, chunk.start_char - overlap_target)
# Snap to sentence boundary if possible
if new_start < chunk.start_char:
region = text[new_start:chunk.start_char]
sentence_matches = list(_SENTENCE_END.finditer(region))
if sentence_matches:
# Use the latest sentence break in the overlap region
last_match = sentence_matches[-1]
candidate = new_start + last_match.end()
if candidate < chunk.start_char:
new_start = candidate
overlap_left = chunk.start_char - new_start
if i < len(chunks) - 1:
# Calculate how much the next chunk will overlap into this one
next_chunk = chunks[i + 1]
overlap_target = min(strategy.overlap_chars, len(text) - next_chunk.start_char)
potential_overlap_start = max(0, next_chunk.start_char - overlap_target)
# The overlap_right for this chunk = how much the next chunk's
# overlap will include from this chunk's content
overlap_right = chunk.end_char - max(potential_overlap_start, chunk.start_char)
overlap_right = max(0, overlap_right)
new_text = text[new_start:new_end]
if not new_text.strip():
result.append(chunk)
continue
result.append(DocumentChunk(
chunk_id=f"{document_id}:{new_start}",
document_id=document_id,
document_type=document_type,
section_path=chunk.section_path,
speaker=chunk.speaker,
start_char=new_start,
end_char=new_end,
text=new_text,
overlap_left=overlap_left,
overlap_right=overlap_right,
boilerplate_score=chunk.boilerplate_score,
))
return result
def _apply_metadata(
self,
chunks: list[DocumentChunk],
text: str,
document_type: str,
) -> None:
"""Apply section_path and speaker metadata to chunks in place."""
if document_type.lower() == "transcript":
self._apply_speaker_metadata(chunks, text)
elif document_type.lower() == "filing":
self._apply_filing_sections(chunks, text)
def _apply_speaker_metadata(
self,
chunks: list[DocumentChunk],
text: str,
) -> None:
"""Find speaker turns and assign speaker labels to transcript chunks."""
speakers: list[tuple[int, str]] = []
for match in _SPEAKER_PATTERN.finditer(text):
speakers.append((match.start(), match.group(1).strip()))
if not speakers:
return
for chunk in chunks:
# Find the most recent speaker before or within this chunk
current_speaker = None
for pos, name in speakers:
if pos <= chunk.end_char:
if pos < chunk.start_char:
current_speaker = name
else:
current_speaker = name
else:
break
chunk.speaker = current_speaker
def _apply_filing_sections(
self,
chunks: list[DocumentChunk],
text: str,
) -> None:
"""Find filing section headers and assign section_path to chunks."""
sections: list[tuple[int, str]] = []
for match in _FILING_SECTION_PATTERN.finditer(text):
sections.append((match.start(), match.group(1).strip()))
if not sections:
return
for chunk in chunks:
# Build section path from all sections that precede or start within this chunk
path: list[str] = []
for pos, title in sections:
if pos < chunk.end_char:
# Keep track of the most recent section(s)
if pos <= chunk.start_char:
path = [title]
else:
path.append(title)
else:
break
chunk.section_path = path