Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/scribae/brief.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import logging
import re
from collections.abc import Callable
from dataclasses import dataclass
Expand All @@ -20,6 +21,8 @@
from .project import ProjectConfig
from .prompts.brief import SYSTEM_PROMPT, PromptBundle, build_prompt_bundle

logger = logging.getLogger(__name__)

__all__ = [
# re-exports for tests and public API
"NoteDetails",
Expand Down Expand Up @@ -234,6 +237,7 @@ def generate_brief(
language_detector: Callable[[str], str] | None = None,
) -> SeoBrief:
"""Run the LLM call and return a validated SeoBrief."""
logger.debug("Generating brief with model '%s'", model_name)
resolved_settings = settings or OpenAISettings.from_env()
llm_agent: Agent[None, SeoBrief] = (
_create_agent(model_name, resolved_settings, temperature=temperature, top_p=top_p, seed=seed)
Expand Down Expand Up @@ -274,6 +278,7 @@ def generate_brief(
raise BriefLLMError(f"LLM request failed: {exc}") from exc

_report(reporter, "LLM call complete, structured brief validated.")
logger.debug("Brief generation completed successfully")
return brief


Expand Down
2 changes: 2 additions & 0 deletions src/scribae/brief_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .brief import BriefingError
from .cli_output import echo_info, is_quiet, secho_info
from .llm import DEFAULT_MODEL_NAME
from .logging_config import setup_logging
from .project import load_default_project, load_project


Expand Down Expand Up @@ -170,6 +171,7 @@ def brief_command(
),
) -> None:
"""CLI handler for `scribae brief`."""
setup_logging(verbose=verbose and not is_quiet())
_validate_output_options(out, json_output, dry_run=dry_run, idea_all=idea_all, out_dir=out_dir)
if (idea or idea_all) and ideas is None:
raise typer.BadParameter("--ideas is required when selecting ideas.", param_hint="--ideas")
Expand Down
5 changes: 5 additions & 0 deletions src/scribae/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import logging
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass
Expand All @@ -21,6 +22,8 @@
from .prompts.feedback import FEEDBACK_SYSTEM_PROMPT, FeedbackPromptBundle, build_feedback_prompt_bundle
from .prompts.feedback_categories import CATEGORY_DEFINITIONS

logger = logging.getLogger(__name__)

# Pattern to match emoji characters across common Unicode ranges
_EMOJI_PATTERN = re.compile(
"["
Expand Down Expand Up @@ -366,6 +369,7 @@ def generate_feedback_report(
language_detector: Callable[[str], str] | None = None,
) -> FeedbackReport:
"""Generate the structured feedback report via the LLM."""
logger.debug("Generating feedback report with model '%s'", model_name)
prompts = prompts or build_prompt_bundle(context)

resolved_settings = OpenAISettings.from_env()
Expand Down Expand Up @@ -402,6 +406,7 @@ def generate_feedback_report(

# Remap any out-of-scope categories to "other"
report = _normalize_finding_categories(report, context.focus)
logger.debug("Feedback report generation completed successfully")
return report


Expand Down
2 changes: 2 additions & 0 deletions src/scribae/feedback_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
save_prompt_artifacts,
)
from .llm import DEFAULT_MODEL_NAME
from .logging_config import setup_logging
from .project import load_default_project, load_project


Expand Down Expand Up @@ -141,6 +142,7 @@ def feedback_command(
scribae feedback --body draft.md --brief brief.json --format json --out feedback.json
scribae feedback --body draft.md --brief brief.json --section 1..3 --focus seo
"""
setup_logging(verbose=verbose and not is_quiet())
reporter = (lambda msg: typer.secho(msg, err=True)) if verbose and not is_quiet() else None

try:
Expand Down
5 changes: 5 additions & 0 deletions src/scribae/idea.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import logging
import re
from collections.abc import Callable
from dataclasses import dataclass
Expand All @@ -25,6 +26,8 @@
from .project import ProjectConfig
from .prompts.idea import IDEA_SYSTEM_PROMPT, IdeaPromptBundle, build_idea_prompt_bundle

logger = logging.getLogger(__name__)


class IdeaError(Exception):
"""Raised when ideas cannot be generated."""
Expand Down Expand Up @@ -151,6 +154,7 @@ def generate_ideas(
language_detector: Callable[[str], str] | None = None,
) -> IdeaList:
"""Run the LLM call and return validated ideas."""
logger.debug("Generating ideas with model '%s'", model_name)

resolved_settings = settings or OpenAISettings.from_env()
llm_agent: Agent[None, IdeaList] = (
Expand Down Expand Up @@ -189,6 +193,7 @@ def generate_ideas(
raise IdeaLLMError(f"LLM request failed: {exc}") from exc

_report(reporter, "LLM call complete, ideas validated.")
logger.debug("Idea generation completed successfully")
return ideas


Expand Down
3 changes: 2 additions & 1 deletion src/scribae/idea_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .cli_output import echo_info, is_quiet, secho_info
from .idea import IdeaError, generate_ideas, prepare_context, render_json, save_prompt_artifacts
from .llm import DEFAULT_MODEL_NAME
from .logging_config import setup_logging
from .project import load_default_project, load_project


Expand Down Expand Up @@ -122,7 +123,7 @@ def idea_command(
),
) -> None:
"""CLI handler for `scribae idea`."""

setup_logging(verbose=verbose and not is_quiet())
_validate_output_options(out, json_output, dry_run=dry_run)

reporter = (lambda msg: typer.secho(msg, err=True)) if verbose and not is_quiet() else None
Expand Down
48 changes: 48 additions & 0 deletions src/scribae/logging_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

import logging
import os
import sys
from pathlib import Path

_LOGGER_NAME = "scribae"


def setup_logging(*, verbose: bool = False, log_file: Path | None = None) -> logging.Logger:
"""Configure Scribae logger handlers and level.

Calling this function repeatedly is safe; existing handlers are replaced so
duplicate records are not emitted.
"""

logger = logging.getLogger(_LOGGER_NAME)
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
logger.propagate = False

for handler in list(logger.handlers):
logger.removeHandler(handler)

formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")

stream_handler = logging.StreamHandler(sys.stderr)
stream_handler.setFormatter(formatter)
stream_handler.setLevel(logging.DEBUG if verbose else logging.WARNING)
logger.addHandler(stream_handler)

resolved_log_file = log_file
if resolved_log_file is None:
raw_log_file = os.environ.get("SCRIBAE_LOG_FILE")
if raw_log_file:
resolved_log_file = Path(raw_log_file).expanduser()

if resolved_log_file is not None:
resolved_log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(resolved_log_file, encoding="utf-8")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
logger.addHandler(file_handler)

return logger


__all__ = ["setup_logging"]
2 changes: 2 additions & 0 deletions src/scribae/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .feedback_cli import feedback_command
from .idea_cli import idea_command
from .init_cli import init_command
from .logging_config import setup_logging
from .meta_cli import meta_command
from .refine_cli import refine_command
from .translate_cli import translate_command
Expand Down Expand Up @@ -42,6 +43,7 @@ def app_callback(
),
) -> None:
"""Root Scribae CLI callback."""
setup_logging()
ctx.obj = {"quiet": quiet}
if no_color or "NO_COLOR" in os.environ:
context = click.get_current_context(silent=True)
Expand Down
5 changes: 5 additions & 0 deletions src/scribae/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import logging
import re
from collections.abc import Callable
from dataclasses import dataclass
Expand All @@ -26,6 +27,8 @@
build_meta_prompt_bundle,
)

logger = logging.getLogger(__name__)


class MetaError(Exception):
"""Base class for meta-command failures."""
Expand Down Expand Up @@ -237,6 +240,7 @@ def generate_metadata(
language_detector: Callable[[str], str] | None = None,
) -> ArticleMeta:
"""Generate final article metadata, calling the LLM when needed."""
logger.debug("Generating metadata with overwrite mode '%s'", context.overwrite)
prompts = prompts or build_prompt_bundle(context)
needs_llm, reason = _needs_llm(context, force_llm_on_missing=force_llm_on_missing)

Expand Down Expand Up @@ -291,6 +295,7 @@ def generate_metadata(
merged.reading_time = context.body.reading_time
merged.tags = _apply_allowed_tags(merged.tags, context.project.get("allowed_tags"))

logger.debug("Metadata generation completed successfully")
return merged


Expand Down
2 changes: 2 additions & 0 deletions src/scribae/meta_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .cli_output import echo_info, is_quiet, secho_info
from .llm import DEFAULT_MODEL_NAME
from .logging_config import setup_logging
from .meta import (
ArticleMeta,
MetaBriefError,
Expand Down Expand Up @@ -130,6 +131,7 @@ def meta_command(
),
) -> None:
"""CLI handler for `scribae meta`."""
setup_logging(verbose=verbose and not is_quiet())
reporter = (lambda msg: typer.secho(msg, err=True)) if verbose and not is_quiet() else None

try:
Expand Down
9 changes: 6 additions & 3 deletions src/scribae/refine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import logging
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass
Expand All @@ -27,6 +28,8 @@

Reporter = Callable[[str], None] | None

logger = logging.getLogger(__name__)


class RefiningError(Exception):
"""Base class for refine command failures."""
Expand Down Expand Up @@ -245,6 +248,7 @@ def refine_draft(
language_detector: Callable[[str], str] | None = None,
) -> tuple[str, str | None]:
"""Refine a draft and optionally return a changelog."""
logger.debug("Refining draft with model '%s'", model_name)
draft = parse_draft(context.draft_text)
outline = outline_sections(context.brief)
refined_sections = _prepare_sections(draft, outline, preserve_anchors=preserve_anchors)
Expand Down Expand Up @@ -336,6 +340,7 @@ def refine_draft(
reporter=reporter,
)

logger.debug("Draft refinement completed successfully")
return markdown, changelog_text


Expand Down Expand Up @@ -514,9 +519,7 @@ def _prepare_sections(
draft_section = draft.sections[index - 1]
anchor = draft_section.anchor if preserve_anchors else None
heading = _compose_heading(title, anchor=anchor)
refined_sections.append(
RefinedSection(index=index, title=title, heading=heading, body=draft_section.body)
)
refined_sections.append(RefinedSection(index=index, title=title, heading=heading, body=draft_section.body))
if not refined_sections:
raise RefiningValidationError("No outline sections selected.")
return refined_sections
Expand Down
2 changes: 2 additions & 0 deletions src/scribae/refine_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .cli_output import echo_info, is_quiet, secho_info
from .llm import DEFAULT_MODEL_NAME
from .logging_config import setup_logging
from .project import load_default_project, load_project
from .refine import (
EvidenceMode,
Expand Down Expand Up @@ -138,6 +139,7 @@ def refine_command(
),
) -> None:
"""CLI handler for `scribae refine`."""
setup_logging(verbose=verbose and not is_quiet())
if dry_run and (out is not None or save_prompt is not None or changelog is not None):
raise typer.BadParameter(
"--dry-run cannot be combined with --out/--save-prompt/--changelog.",
Expand Down
5 changes: 5 additions & 0 deletions src/scribae/translate/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
Expand All @@ -12,6 +13,8 @@

DebugCallback = Callable[[dict[str, Any]], None] | None

logger = logging.getLogger(__name__)


@dataclass
class ToneProfile:
Expand Down Expand Up @@ -73,6 +76,7 @@ def _report_block(
self._report(f"{prefix}: {message}")

def translate(self, text: str, cfg: TranslationConfig) -> str:
logger.debug("Starting translation pipeline: %s -> %s", cfg.source_lang, cfg.target_lang)
self._report(f"Starting translation: {cfg.source_lang} -> {cfg.target_lang}")
self._report("Segmenting markdown into blocks...")
blocks = self.segmenter.segment(text)
Expand All @@ -81,6 +85,7 @@ def translate(self, text: str, cfg: TranslationConfig) -> str:
self._report("Reconstructing translated document...")
result = self.segmenter.reconstruct(translated_blocks)
self._report("Translation complete")
logger.debug("Translation pipeline completed")
return result

def translate_blocks(self, blocks: list[TextBlock], cfg: TranslationConfig) -> list[TextBlock]:
Expand Down
3 changes: 2 additions & 1 deletion src/scribae/translate_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from scribae.cli_output import echo_info, is_quiet, secho_info
from scribae.language import LanguageResolutionError, detect_language, normalize_language
from scribae.llm import DEFAULT_MODEL_NAME
from scribae.logging_config import setup_logging
from scribae.project import load_default_project, load_project
from scribae.translate import (
LLMPostEditor,
Expand Down Expand Up @@ -60,7 +61,6 @@ def _configure_library_logging() -> None:
pass



def _load_glossary(path: Path | None) -> dict[str, str]:
if path is None:
return {}
Expand Down Expand Up @@ -205,6 +205,7 @@ def translate(
),
) -> None:
"""Translate a Markdown file using offline MT + local post-edit."""
setup_logging(verbose=verbose and not is_quiet())
reporter = (lambda msg: typer.secho(msg, err=True)) if verbose and not is_quiet() else None
_configure_library_logging()

Expand Down
Loading