Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
218 lines
7.1 KiB
Python
218 lines
7.1 KiB
Python
"""Main processing pipeline orchestrating discovery → extraction → enrichment → linking → output.
|
|
|
|
Handles:
|
|
- Progress tracking (current file / total)
|
|
- Per-file error handling (log + skip + continue)
|
|
- Summary reporting (processed, success, failed counts)
|
|
- Dry-run mode
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from ingestion.config import IngestionConfig
|
|
from ingestion.discovery import discover_files
|
|
from ingestion.enrichment.agent import EnrichmentAgent
|
|
from ingestion.enrichment.models import EnrichmentResult
|
|
from ingestion.extraction import get_extractor
|
|
from ingestion.integrations.git import commit_imported_notes
|
|
from ingestion.integrations.orgmylife import OrgMyLifeClient
|
|
from ingestion.linking.linker import generate_wiki_links
|
|
from ingestion.linking.stubs import create_stub_pages
|
|
from ingestion.output.naming import generate_filename, resolve_collision
|
|
from ingestion.output.renderer import render_note
|
|
from ingestion.output.router import route_to_folder
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class ProcessResult:
|
|
"""Result of processing a single file."""
|
|
|
|
source_file: Path
|
|
output_file: Path | None = None
|
|
success: bool = False
|
|
error: str | None = None
|
|
stub_pages: list[Path] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class BatchResult:
|
|
"""Summary of a batch processing run."""
|
|
|
|
total: int = 0
|
|
success: int = 0
|
|
failed: int = 0
|
|
results: list[ProcessResult] = field(default_factory=list)
|
|
|
|
|
|
def process_file(
|
|
file_path: Path,
|
|
config: IngestionConfig,
|
|
enrichment_agent: EnrichmentAgent | None = None,
|
|
dry_run: bool = False,
|
|
no_commit: bool = False,
|
|
create_tasks: bool = False,
|
|
output_dir: Path | None = None,
|
|
) -> ProcessResult:
|
|
"""Process a single file through the full ingestion pipeline.
|
|
|
|
Steps: extract → enrich → link → render → write → (optional) git commit
|
|
|
|
Args:
|
|
file_path: Path to the source file.
|
|
config: Ingestion configuration.
|
|
enrichment_agent: Optional pre-configured enrichment agent.
|
|
dry_run: If True, don't write any files.
|
|
no_commit: If True, skip git commit.
|
|
create_tasks: If True, create OrgMyLife tasks for action items.
|
|
output_dir: Optional override for output directory.
|
|
|
|
Returns:
|
|
ProcessResult with success status and output path.
|
|
"""
|
|
result = ProcessResult(source_file=file_path)
|
|
|
|
try:
|
|
# 1. Extract text
|
|
extractor = get_extractor(file_path, config)
|
|
if extractor is None:
|
|
result.error = f"No extractor available for {file_path.suffix}"
|
|
return result
|
|
|
|
extraction = extractor.extract(file_path)
|
|
if not extraction.text.strip():
|
|
result.error = "No text extracted from file"
|
|
return result
|
|
|
|
# 2. Enrich with LLM
|
|
if enrichment_agent is None:
|
|
enrichment_agent = EnrichmentAgent(config)
|
|
|
|
enrichment = enrichment_agent.enrich(extraction.text)
|
|
|
|
# 3. Generate wiki-links
|
|
linked_text = generate_wiki_links(extraction.text, enrichment.entities)
|
|
|
|
# 4. Render output
|
|
rendered = render_note(enrichment, linked_text, file_path)
|
|
|
|
# 5. Determine output location
|
|
notes_dir = Path(config.notes_dir)
|
|
target_folder = route_to_folder(enrichment.category, notes_dir, output_dir)
|
|
|
|
# 6. Generate filename
|
|
date_entities = [e for e in enrichment.entities if e.type == "date"]
|
|
date_str = date_entities[0].value if date_entities else None
|
|
filename = generate_filename(enrichment.title, date_str)
|
|
output_path = resolve_collision(target_folder, filename)
|
|
|
|
if dry_run:
|
|
logger.info("[DRY RUN] Would write: %s", output_path)
|
|
result.output_file = output_path
|
|
result.success = True
|
|
return result
|
|
|
|
# 7. Write file
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(rendered, encoding="utf-8")
|
|
result.output_file = output_path
|
|
logger.info("Written: %s", output_path)
|
|
|
|
# 8. Create stub pages
|
|
stubs = create_stub_pages(enrichment.entities, notes_dir)
|
|
result.stub_pages = stubs
|
|
|
|
# 9. Create OrgMyLife tasks if requested
|
|
if create_tasks and enrichment.action_items and config.orgmylife_api_key:
|
|
_create_tasks(enrichment, output_path, config)
|
|
|
|
result.success = True
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to process %s: %s", file_path, e)
|
|
result.error = str(e)
|
|
|
|
return result
|
|
|
|
|
|
def process_batch(
|
|
paths: list[Path],
|
|
config: IngestionConfig,
|
|
dry_run: bool = False,
|
|
no_commit: bool = False,
|
|
create_tasks: bool = False,
|
|
output_dir: Path | None = None,
|
|
progress_callback=None,
|
|
) -> BatchResult:
|
|
"""Process a batch of files through the pipeline.
|
|
|
|
Args:
|
|
paths: List of file paths to process.
|
|
config: Ingestion configuration.
|
|
dry_run: If True, don't write any files.
|
|
no_commit: If True, skip git commit.
|
|
create_tasks: If True, create OrgMyLife tasks.
|
|
output_dir: Optional override for output directory.
|
|
progress_callback: Optional callable(current, total, filename).
|
|
|
|
Returns:
|
|
BatchResult with summary counts and per-file results.
|
|
"""
|
|
batch = BatchResult(total=len(paths))
|
|
enrichment_agent = EnrichmentAgent(config)
|
|
written_files: list[Path] = []
|
|
|
|
for i, file_path in enumerate(paths, 1):
|
|
if progress_callback:
|
|
progress_callback(i, len(paths), file_path.name)
|
|
|
|
logger.info("Processing [%d/%d]: %s", i, len(paths), file_path.name)
|
|
|
|
result = process_file(
|
|
file_path=file_path,
|
|
config=config,
|
|
enrichment_agent=enrichment_agent,
|
|
dry_run=dry_run,
|
|
no_commit=True, # Commit once at end
|
|
create_tasks=create_tasks,
|
|
output_dir=output_dir,
|
|
)
|
|
|
|
batch.results.append(result)
|
|
if result.success:
|
|
batch.success += 1
|
|
if result.output_file:
|
|
written_files.append(result.output_file)
|
|
written_files.extend(result.stub_pages)
|
|
else:
|
|
batch.failed += 1
|
|
logger.warning("Skipped %s: %s", file_path.name, result.error)
|
|
|
|
# Git commit all at once (unless dry-run or no-commit)
|
|
if not dry_run and not no_commit and config.auto_commit and written_files:
|
|
commit_imported_notes(written_files, source_type="batch-import")
|
|
|
|
return batch
|
|
|
|
|
|
def _create_tasks(
|
|
enrichment: EnrichmentResult, note_path: Path, config: IngestionConfig
|
|
) -> None:
|
|
"""Create OrgMyLife tasks for action items."""
|
|
try:
|
|
client = OrgMyLifeClient(
|
|
base_url=config.orgmylife_api_url,
|
|
api_key=config.orgmylife_api_key,
|
|
)
|
|
for item in enrichment.action_items:
|
|
client.create_task(
|
|
title=item.description,
|
|
source_url=str(note_path),
|
|
description=f"From note: {enrichment.title}",
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Failed to create OrgMyLife tasks: %s", e)
|