"""Wiki-link generation from detected entities. Converts entity references to SilverBullet-compatible wiki-links: - Person → [[people/firstname-lastname]] - Project → [[projects/project-slug]] Only the first occurrence of each entity is linked; subsequent mentions remain plain text. """ import logging from slugify import slugify from ingestion.enrichment.models import Entity logger = logging.getLogger(__name__) def _entity_to_wikilink(entity: Entity) -> str: """Convert an entity to its wiki-link representation.""" slug = slugify(entity.value) if entity.type == "person": return f"[[people/{slug}]]" elif entity.type == "project": return f"[[projects/{slug}]]" return "" def generate_wiki_links(text: str, entities: list[Entity]) -> str: """Insert wiki-links into text for person and project entities. Only the first occurrence of each entity's value is converted to a wiki-link. Dates and action_items are skipped (handled by frontmatter/renderer). Args: text: The document body text. entities: List of detected entities from enrichment. Returns: Text with wiki-links inserted at first occurrence of each entity. """ # Filter to linkable entity types linkable = [e for e in entities if e.type in ("person", "project")] # Track which entity values we've already linked (case-insensitive) linked_values: set[str] = set() for entity in linkable: lower_val = entity.value.lower() if lower_val in linked_values: continue # Find first occurrence and replace it idx = text.find(entity.value) if idx == -1: # Try case-insensitive search idx = text.lower().find(lower_val) if idx == -1: continue # Use the actual text at that position for replacement original_text = text[idx : idx + len(entity.value)] else: original_text = entity.value wikilink = _entity_to_wikilink(entity) if wikilink: # Replace only the first occurrence text = text[:idx] + wikilink + text[idx + len(original_text) :] linked_values.add(lower_val) return text