/** * Relationship Confidence Scoring * * Scores the confidence of detected relationships based on multiple factors: * - Entity proximity in text * - Entity confidence scores * - Pattern matches * - Structural analysis * * PRODUCTION-READY - NO MOCKS, NO STUBS, REAL IMPLEMENTATION */ import { ExtractedEntity } from './entityExtractor.js' import { VerbType } from '../types/graphTypes.js' import { RelationEvidence } from '../types/brainy.types.js' /** * Detected relationship with confidence */ export interface DetectedRelationship { sourceEntity: ExtractedEntity targetEntity: ExtractedEntity verbType: VerbType confidence: number evidence: RelationEvidence } /** * Configuration for relationship detection */ export interface RelationshipDetectionConfig { minConfidence?: number // Minimum confidence to return (default: 0.5) maxDistance?: number // Maximum token distance between entities (default: 50) useProximityBoost?: boolean // Boost score based on proximity (default: true) usePatternMatching?: boolean // Use verb pattern matching (default: true) useStructuralAnalysis?: boolean // Analyze sentence structure (default: true) } /** * Relationship confidence scorer */ export class RelationshipConfidenceScorer { private config: Required constructor(config: RelationshipDetectionConfig = {}) { this.config = { minConfidence: config.minConfidence || 0.5, maxDistance: config.maxDistance || 50, useProximityBoost: config.useProximityBoost !== false, usePatternMatching: config.usePatternMatching !== false, useStructuralAnalysis: config.useStructuralAnalysis !== false } } /** * Score a potential relationship between two entities */ scoreRelationship( source: ExtractedEntity, target: ExtractedEntity, verbType: VerbType, context: string ): { confidence: number, evidence: RelationEvidence } { let confidence = 0.5 // Base confidence // Evidence tracking const reasoningParts: string[] = [] // Factor 1: Proximity boost (closer entities = higher confidence) if (this.config.useProximityBoost) { const proximityBoost = this.calculateProximityBoost(source, target) confidence += proximityBoost if (proximityBoost > 0) { reasoningParts.push( `Entities are close together (boost: +${proximityBoost.toFixed(2)})` ) } } // Factor 2: Entity confidence boost const entityConfidence = (source.confidence + target.confidence) / 2 const entityBoost = (entityConfidence - 0.5) * 0.2 // Scale to 0-0.2 confidence *= (1 + entityBoost) if (entityBoost > 0) { reasoningParts.push( `High entity confidence (boost: ${entityBoost.toFixed(2)})` ) } // Factor 3: Pattern match boost if (this.config.usePatternMatching) { const patternBoost = this.checkVerbPattern(source, target, verbType, context) confidence += patternBoost if (patternBoost > 0) { reasoningParts.push( `Matches relationship pattern (boost: +${patternBoost.toFixed(2)})` ) } } // Factor 4: Structural boost (same sentence, clause, etc.) if (this.config.useStructuralAnalysis) { const structuralBoost = this.analyzeStructure(source, target, context) confidence += structuralBoost if (structuralBoost > 0) { reasoningParts.push( `Structural relationship (boost: +${structuralBoost.toFixed(2)})` ) } } // Cap confidence at 1.0 confidence = Math.min(confidence, 1.0) // Extract source text evidence const start = Math.min(source.position.start, target.position.start) const end = Math.max(source.position.end, target.position.end) const evidence: RelationEvidence = { sourceText: context.substring(start, end), position: { start, end }, method: 'neural', reasoning: reasoningParts.join('; ') } return { confidence, evidence } } /** * Calculate proximity boost based on distance between entities */ private calculateProximityBoost( source: ExtractedEntity, target: ExtractedEntity ): number { const distance = Math.abs(source.position.start - target.position.start) if (distance === 0) return 0 // Same position, not meaningful // Very close (< 20 chars): +0.2 if (distance < 20) return 0.2 // Close (< 50 chars): +0.1 if (distance < 50) return 0.1 // Medium (< 100 chars): +0.05 if (distance < 100) return 0.05 // Far (> 100 chars): no boost return 0 } /** * Check if entities match a verb pattern */ private checkVerbPattern( source: ExtractedEntity, target: ExtractedEntity, verbType: VerbType, context: string ): number { const contextBetween = this.getContextBetween(source, target, context) const contextLower = contextBetween.toLowerCase() // Verb-specific patterns const patterns: Record = { [VerbType.Creates]: ['creates', 'made', 'built', 'developed', 'produces'], [VerbType.Owns]: ['owns', 'belongs to', 'possessed by', 'has'], [VerbType.Contains]: ['contains', 'includes', 'has', 'holds'], [VerbType.Requires]: ['requires', 'needs', 'depends on', 'relies on'], [VerbType.Uses]: ['uses', 'utilizes', 'employs', 'applies'], [VerbType.Supervises]: ['manages', 'oversees', 'supervises', 'controls'], [VerbType.Causes]: ['influences', 'affects', 'impacts', 'shapes', 'causes'], [VerbType.DependsOn]: ['depends on', 'relies on', 'based on'], [VerbType.Modifies]: ['modifies', 'changes', 'alters', 'updates'], [VerbType.References]: ['references', 'cites', 'mentions', 'refers to'] } const verbPatterns = patterns[verbType] || [] for (const pattern of verbPatterns) { if (contextLower.includes(pattern)) { return 0.2 // Strong pattern match } } return 0 // No pattern match } /** * Analyze structural relationship */ private analyzeStructure( source: ExtractedEntity, target: ExtractedEntity, context: string ): number { const contextBetween = this.getContextBetween(source, target, context) // Same sentence (no sentence-ending punctuation between them) if (!contextBetween.match(/[.!?]/)) { return 0.1 } // Same paragraph (single newline between them) if (!contextBetween.match(/\n\n/)) { return 0.05 } return 0 } /** * Get context text between two entities */ private getContextBetween( source: ExtractedEntity, target: ExtractedEntity, context: string ): string { const start = Math.min(source.position.end, target.position.end) const end = Math.max(source.position.start, target.position.start) if (start >= end) return '' return context.substring(start, end) } /** * Detect relationships between a list of entities */ detectRelationships( entities: ExtractedEntity[], context: string, verbHints?: VerbType[] ): DetectedRelationship[] { const relationships: DetectedRelationship[] = [] const verbs = verbHints || [ VerbType.Creates, VerbType.Uses, VerbType.Contains, VerbType.Requires, VerbType.RelatedTo ] // Check all entity pairs for (let i = 0; i < entities.length; i++) { for (let j = i + 1; j < entities.length; j++) { const source = entities[i] const target = entities[j] // Check distance const distance = Math.abs(source.position.start - target.position.start) if (distance > this.config.maxDistance) { continue // Too far apart } // Try each verb type for (const verbType of verbs) { const { confidence, evidence } = this.scoreRelationship( source, target, verbType, context ) if (confidence >= this.config.minConfidence) { relationships.push({ sourceEntity: source, targetEntity: target, verbType, confidence, evidence }) } } } } // Sort by confidence (highest first) relationships.sort((a, b) => b.confidence - a.confidence) return relationships } } /** * Convenience function to score a single relationship */ export function scoreRelationshipConfidence( source: ExtractedEntity, target: ExtractedEntity, verbType: VerbType, context: string, config?: RelationshipDetectionConfig ): { confidence: number, evidence: RelationEvidence } { const scorer = new RelationshipConfidenceScorer(config) return scorer.scoreRelationship(source, target, verbType, context) } /** * Convenience function to detect all relationships in text */ export function detectRelationshipsWithConfidence( entities: ExtractedEntity[], context: string, config?: RelationshipDetectionConfig ): DetectedRelationship[] { const scorer = new RelationshipConfidenceScorer(config) return scorer.detectRelationships(entities, context) }